@yoyomq/ije-core 0.1.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 +30 -0
- package/dist/chatClient.d.ts +22 -0
- package/dist/chatClient.js +16 -0
- package/dist/httpClient.d.ts +18 -0
- package/dist/httpClient.js +55 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +75 -0
- package/dist/mqttManager.d.ts +15 -0
- package/dist/mqttManager.js +133 -0
- package/dist/src/chatClient.d.ts +22 -0
- package/dist/src/chatClient.js +32 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.js +59 -0
- package/dist/src/mqttManager.d.ts +9 -0
- package/dist/src/mqttManager.js +22 -0
- package/dist/tripsClient.d.ts +118 -0
- package/dist/tripsClient.js +97 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="../../assets/yoyo.svg" width="56" height="56" alt="Yoyo" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@yoyomq/ije-core</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">Runtime core for the Ije SDK by <strong>Yoyo</strong>.</p>
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
This package exposes the `Ije` singleton — the runtime that powers every Ije
|
|
12
|
+
widget. It handles initialization, theming, the real-time MQTT connection, and the
|
|
13
|
+
insights/chat API.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Ije } from '@yoyomq/ije-core';
|
|
17
|
+
|
|
18
|
+
await Ije.init({ apiKey: 'YOUR_YOYO_API_KEY', theme: { primaryColor: '#8A2BE2' } });
|
|
19
|
+
|
|
20
|
+
// Natural-language insights
|
|
21
|
+
const res = await Ije.chat.ask('How many devices reported in the last hour?');
|
|
22
|
+
|
|
23
|
+
// Raw real-time streams (the UI widgets use these for you)
|
|
24
|
+
Ije.mqtt.subscribe('device/truck-001/telemetry', (p) => console.log(p.speed));
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It's usually paired with [`@yoyomq/ije-ui`](../ui) for the drop-in `<ije-*>`
|
|
28
|
+
components.
|
|
29
|
+
|
|
30
|
+
📖 **Full documentation:** see the [Ije SDK README](../../README.md).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SdkConfig } from './index';
|
|
2
|
+
export interface ChatChartSpec {
|
|
3
|
+
chart_type: 'bar' | 'line' | 'pie' | 'scatter' | 'table';
|
|
4
|
+
title: string;
|
|
5
|
+
labels: string[];
|
|
6
|
+
datasets: Array<{
|
|
7
|
+
label: string;
|
|
8
|
+
data: number[];
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export interface ChatResponse {
|
|
12
|
+
session_id: string;
|
|
13
|
+
answer: string;
|
|
14
|
+
chart?: ChatChartSpec;
|
|
15
|
+
}
|
|
16
|
+
export declare class IjeChatClient {
|
|
17
|
+
private sessionId;
|
|
18
|
+
private http;
|
|
19
|
+
_setConfig(config: SdkConfig): void;
|
|
20
|
+
ask(question: string): Promise<ChatResponse>;
|
|
21
|
+
resetSession(): void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IjeHttpClient } from './httpClient';
|
|
2
|
+
export class IjeChatClient {
|
|
3
|
+
sessionId = null;
|
|
4
|
+
http = new IjeHttpClient();
|
|
5
|
+
_setConfig(config) {
|
|
6
|
+
this.http._setConfig(config);
|
|
7
|
+
}
|
|
8
|
+
async ask(question) {
|
|
9
|
+
const data = await this.http.post('/public/api/v1/apigateway/mimir/insights/query', { session_id: this.sessionId, question });
|
|
10
|
+
this.sessionId = data.session_id;
|
|
11
|
+
return data;
|
|
12
|
+
}
|
|
13
|
+
resetSession() {
|
|
14
|
+
this.sessionId = null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SdkConfig } from './index';
|
|
2
|
+
type ScalarParam = string | number | boolean | undefined | null;
|
|
3
|
+
interface GetOptions {
|
|
4
|
+
params?: Record<string, ScalarParam>;
|
|
5
|
+
arrayParams?: Record<string, (string | number)[]>;
|
|
6
|
+
}
|
|
7
|
+
interface PostOptions {
|
|
8
|
+
}
|
|
9
|
+
export declare class IjeHttpClient {
|
|
10
|
+
private config;
|
|
11
|
+
_setConfig(config: SdkConfig): void;
|
|
12
|
+
get<T>(path: string, options?: GetOptions): Promise<T>;
|
|
13
|
+
post<T>(path: string, body: unknown, _options?: PostOptions): Promise<T>;
|
|
14
|
+
private buildUrl;
|
|
15
|
+
private buildHeaders;
|
|
16
|
+
private parseResponse;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export class IjeHttpClient {
|
|
2
|
+
config = null;
|
|
3
|
+
_setConfig(config) {
|
|
4
|
+
this.config = config;
|
|
5
|
+
}
|
|
6
|
+
async get(path, options = {}) {
|
|
7
|
+
const url = this.buildUrl(path, options.params, options.arrayParams);
|
|
8
|
+
const response = await fetch(url, {
|
|
9
|
+
headers: this.buildHeaders(),
|
|
10
|
+
});
|
|
11
|
+
return this.parseResponse(response);
|
|
12
|
+
}
|
|
13
|
+
async post(path, body, _options = {}) {
|
|
14
|
+
const url = this.buildUrl(path);
|
|
15
|
+
const response = await fetch(url, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
...this.buildHeaders(),
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify(body),
|
|
22
|
+
});
|
|
23
|
+
return this.parseResponse(response);
|
|
24
|
+
}
|
|
25
|
+
buildUrl(path, params, arrayParams) {
|
|
26
|
+
if (!this.config)
|
|
27
|
+
throw new Error('[Yoyo ije] SDK must be initialized before making requests.');
|
|
28
|
+
const url = new URL(`${this.config.apiUrl}${path}`);
|
|
29
|
+
if (params) {
|
|
30
|
+
for (const [key, value] of Object.entries(params)) {
|
|
31
|
+
if (value != null)
|
|
32
|
+
url.searchParams.append(key, String(value));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (arrayParams) {
|
|
36
|
+
for (const [key, values] of Object.entries(arrayParams)) {
|
|
37
|
+
for (const value of values)
|
|
38
|
+
url.searchParams.append(key, String(value));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return url.toString();
|
|
42
|
+
}
|
|
43
|
+
buildHeaders() {
|
|
44
|
+
if (!this.config)
|
|
45
|
+
throw new Error('[Yoyo ije] SDK must be initialized before making requests.');
|
|
46
|
+
return { YOYO_API_KEY: this.config.apiKey };
|
|
47
|
+
}
|
|
48
|
+
async parseResponse(response) {
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const body = await response.text().catch(() => '');
|
|
51
|
+
throw new Error(`[Yoyo ije] Request failed: ${response.status} ${body}`);
|
|
52
|
+
}
|
|
53
|
+
return response.json();
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IjeMqttManager } from './mqttManager';
|
|
2
|
+
import { IjeChatClient } from './chatClient';
|
|
3
|
+
import { IjeTripsClient } from './tripsClient';
|
|
4
|
+
import { IjeHttpClient } from './httpClient';
|
|
5
|
+
export type { ChatChartSpec, ChatResponse } from './chatClient';
|
|
6
|
+
export type { IjeTrigger, IjeDevice, IjeAggregatedEvent, IjeAggregatedEventDetail, IjeDeviceDataPoint, IjeTriggersResponse, IjeDevicesResponse, IjeAggregatedEventsResponse, IjeDeviceDataResponse, ListAggregatedEventsParams, GetDeviceDataParams, } from './tripsClient';
|
|
7
|
+
export { IjeTripsClient } from './tripsClient';
|
|
8
|
+
export { IjeHttpClient } from './httpClient';
|
|
9
|
+
export interface SdkConfig {
|
|
10
|
+
/**
|
|
11
|
+
* Your Yoyo API key. Used as the `YOYO_API_KEY` header for all API calls
|
|
12
|
+
* and as the MQTT credential for live data streams.
|
|
13
|
+
* Get one from https://yoyomq.com → Settings → API Keys.
|
|
14
|
+
*/
|
|
15
|
+
apiKey: string;
|
|
16
|
+
/**
|
|
17
|
+
* Organization UUID used to build MQTT subscription topics.
|
|
18
|
+
* Populated automatically by init() via GET /public/api/v1/context;
|
|
19
|
+
* override only if you need to bypass that fetch.
|
|
20
|
+
*/
|
|
21
|
+
organizationId?: string;
|
|
22
|
+
theme?: {
|
|
23
|
+
primaryColor?: string;
|
|
24
|
+
fontFamily?: string;
|
|
25
|
+
borderRadius?: string;
|
|
26
|
+
};
|
|
27
|
+
apiUrl?: string;
|
|
28
|
+
mqttUrl?: string;
|
|
29
|
+
/** When true, the SDK logs every incoming MQTT message and coordinate parse result to the console. */
|
|
30
|
+
debug?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare class IjeSDK {
|
|
33
|
+
private static instance;
|
|
34
|
+
config: SdkConfig | null;
|
|
35
|
+
isInitialized: boolean;
|
|
36
|
+
mqtt: IjeMqttManager;
|
|
37
|
+
chat: IjeChatClient;
|
|
38
|
+
trips: IjeTripsClient;
|
|
39
|
+
http: IjeHttpClient;
|
|
40
|
+
private constructor();
|
|
41
|
+
static getInstance(): IjeSDK;
|
|
42
|
+
init(config: SdkConfig): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Access the Ije SDK by Yoyo
|
|
46
|
+
*/
|
|
47
|
+
export declare const Ije: IjeSDK;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { IjeMqttManager } from './mqttManager';
|
|
2
|
+
import { IjeChatClient } from './chatClient';
|
|
3
|
+
import { IjeTripsClient } from './tripsClient';
|
|
4
|
+
import { IjeHttpClient } from './httpClient';
|
|
5
|
+
export { IjeTripsClient } from './tripsClient';
|
|
6
|
+
export { IjeHttpClient } from './httpClient';
|
|
7
|
+
export class IjeSDK {
|
|
8
|
+
static instance;
|
|
9
|
+
config = null;
|
|
10
|
+
isInitialized = false;
|
|
11
|
+
mqtt;
|
|
12
|
+
chat;
|
|
13
|
+
trips;
|
|
14
|
+
http;
|
|
15
|
+
constructor() {
|
|
16
|
+
this.mqtt = new IjeMqttManager();
|
|
17
|
+
this.chat = new IjeChatClient();
|
|
18
|
+
this.trips = new IjeTripsClient();
|
|
19
|
+
this.http = new IjeHttpClient();
|
|
20
|
+
}
|
|
21
|
+
static getInstance() {
|
|
22
|
+
if (!IjeSDK.instance) {
|
|
23
|
+
IjeSDK.instance = new IjeSDK();
|
|
24
|
+
}
|
|
25
|
+
return IjeSDK.instance;
|
|
26
|
+
}
|
|
27
|
+
async init(config) {
|
|
28
|
+
if (this.isInitialized) {
|
|
29
|
+
console.warn('[Yoyo ije] SDK is already initialized');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.config = {
|
|
33
|
+
apiUrl: 'https://api.yoyomq.com',
|
|
34
|
+
mqttUrl: 'wss://mqtt.yoyomq.com',
|
|
35
|
+
// Strip undefined values so callers passing `mqttUrl: undefined` (e.g.
|
|
36
|
+
// when an env var isn't set) don't silently clobber built-in defaults.
|
|
37
|
+
...Object.fromEntries(Object.entries(config).filter(([, v]) => v !== undefined)),
|
|
38
|
+
};
|
|
39
|
+
this.chat._setConfig(this.config);
|
|
40
|
+
this.trips._setConfig(this.config);
|
|
41
|
+
this.http._setConfig(this.config);
|
|
42
|
+
// Resolve the organization UUID so UI widgets can build correct MQTT topics.
|
|
43
|
+
// Non-fatal: widgets fall back to legacy topic format when not available.
|
|
44
|
+
if (!this.config.organizationId) {
|
|
45
|
+
try {
|
|
46
|
+
const ctx = await this.http.get('/public/api/v1/context');
|
|
47
|
+
if (ctx.organization_id) {
|
|
48
|
+
this.config.organizationId = ctx.organization_id;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
console.warn('[Yoyo ije] Could not resolve organization ID; live tracking topic may not match');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.mqtt.setDebug(this.config.debug ?? false);
|
|
56
|
+
// Open the real-time MQTT connection that backs the tracking/telemetry
|
|
57
|
+
// widgets. Non-fatal: connection failures retry in the background so a
|
|
58
|
+
// broker hiccup never breaks init() or the rest of the dashboard.
|
|
59
|
+
if (this.config.mqttUrl) {
|
|
60
|
+
this.mqtt.connect(this.config.mqttUrl, this.config.apiKey);
|
|
61
|
+
}
|
|
62
|
+
if (this.config.theme && typeof document !== 'undefined') {
|
|
63
|
+
const root = document.documentElement;
|
|
64
|
+
if (this.config.theme.primaryColor) {
|
|
65
|
+
root.style.setProperty('--yoyo-primary', this.config.theme.primaryColor);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
this.isInitialized = true;
|
|
69
|
+
console.log('[Yoyo ije] SDK initialized');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Access the Ije SDK by Yoyo
|
|
74
|
+
*/
|
|
75
|
+
export const Ije = IjeSDK.getInstance();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type MessageHandler = (payload: Record<string, any>) => void;
|
|
2
|
+
export declare class IjeMqttManager {
|
|
3
|
+
readonly subscriptions: Map<string, Set<MessageHandler>>;
|
|
4
|
+
private client;
|
|
5
|
+
private debug;
|
|
6
|
+
setDebug(enabled: boolean): void;
|
|
7
|
+
subscribe(topic: string, handler: MessageHandler): void;
|
|
8
|
+
unsubscribe(topic: string, handler: MessageHandler): void;
|
|
9
|
+
dispatch(topic: string, payload: Record<string, any>): void;
|
|
10
|
+
connect(url: string, token: string): void;
|
|
11
|
+
disconnect(): void;
|
|
12
|
+
private brokerSubscribe;
|
|
13
|
+
private parsePayload;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import mqtt from 'mqtt';
|
|
2
|
+
export class IjeMqttManager {
|
|
3
|
+
// Keyed by topic — components subscribe/unsubscribe via these Sets
|
|
4
|
+
subscriptions = new Map();
|
|
5
|
+
client = null;
|
|
6
|
+
debug = false;
|
|
7
|
+
setDebug(enabled) {
|
|
8
|
+
this.debug = enabled;
|
|
9
|
+
}
|
|
10
|
+
subscribe(topic, handler) {
|
|
11
|
+
let handlers = this.subscriptions.get(topic);
|
|
12
|
+
const isNewTopic = !handlers;
|
|
13
|
+
if (!handlers) {
|
|
14
|
+
handlers = new Set();
|
|
15
|
+
this.subscriptions.set(topic, handlers);
|
|
16
|
+
}
|
|
17
|
+
handlers.add(handler);
|
|
18
|
+
// Components routinely subscribe before connect() runs (custom elements
|
|
19
|
+
// upgrade on page load, init() happens later). When that's the case we only
|
|
20
|
+
// register locally here — the 'connect' handler resubscribes every known
|
|
21
|
+
// topic at the broker. If we're already live, subscribe the new topic now.
|
|
22
|
+
if (isNewTopic && this.client?.connected) {
|
|
23
|
+
this.brokerSubscribe(topic);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
unsubscribe(topic, handler) {
|
|
27
|
+
const handlers = this.subscriptions.get(topic);
|
|
28
|
+
if (!handlers)
|
|
29
|
+
return;
|
|
30
|
+
handlers.delete(handler);
|
|
31
|
+
// Once nothing on the page cares about a topic, stop receiving it.
|
|
32
|
+
if (handlers.size === 0) {
|
|
33
|
+
this.subscriptions.delete(topic);
|
|
34
|
+
this.client?.unsubscribe(topic);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Dispatches a message to all subscribers of a topic — used by the real MQTT
|
|
38
|
+
// client (see the 'message' handler in connect) and by the demo mock loop to
|
|
39
|
+
// inject synthetic payloads.
|
|
40
|
+
dispatch(topic, payload) {
|
|
41
|
+
if (this.debug) {
|
|
42
|
+
const count = this.subscriptions.get(topic)?.size ?? 0;
|
|
43
|
+
console.log(`[Yoyo ije][MQTT dispatch] ${topic} → ${count} handler(s)`);
|
|
44
|
+
}
|
|
45
|
+
this.subscriptions.get(topic)?.forEach(h => h(payload));
|
|
46
|
+
}
|
|
47
|
+
connect(url, token) {
|
|
48
|
+
// Idempotent — init() may run more than once and components must not each
|
|
49
|
+
// open their own socket.
|
|
50
|
+
if (this.client)
|
|
51
|
+
return;
|
|
52
|
+
const options = {
|
|
53
|
+
// Matches the production frontend connect path (yoyo-frontend
|
|
54
|
+
// lib/mqtt/mqtt.service.ts): the session JWT is sent as the MQTT
|
|
55
|
+
// username and the password is the static string 'any'.
|
|
56
|
+
username: token,
|
|
57
|
+
password: 'any',
|
|
58
|
+
// Intentionally do not force protocolVersion — the broker negotiates it.
|
|
59
|
+
// The proven frontend path leaves this unset (mqtt.js default 3.1.1);
|
|
60
|
+
// forcing v5 risks rejected connections on a 3.1.1-configured broker.
|
|
61
|
+
reconnectPeriod: 1000,
|
|
62
|
+
connectTimeout: 30_000,
|
|
63
|
+
clean: true,
|
|
64
|
+
};
|
|
65
|
+
let client;
|
|
66
|
+
try {
|
|
67
|
+
client = mqtt.connect(url, options);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.error('[Yoyo ije] MQTT connection failed to start:', err);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.client = client;
|
|
74
|
+
client.on('connect', () => {
|
|
75
|
+
console.log(`[Yoyo ije] MQTT connected (${url})`);
|
|
76
|
+
// (Re)subscribe to every topic a component has registered interest in.
|
|
77
|
+
// Covers both the initial connect and any reconnect after a drop.
|
|
78
|
+
for (const topic of this.subscriptions.keys()) {
|
|
79
|
+
this.brokerSubscribe(topic);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
client.on('message', (topic, message) => {
|
|
83
|
+
const payload = this.parsePayload(message);
|
|
84
|
+
if (this.debug) {
|
|
85
|
+
console.log('[Yoyo ije][MQTT]', topic, payload ?? `<unparseable: ${new TextDecoder().decode(message)}>`);
|
|
86
|
+
}
|
|
87
|
+
if (payload)
|
|
88
|
+
this.dispatch(topic, payload);
|
|
89
|
+
});
|
|
90
|
+
client.on('error', err => console.error('[Yoyo ije] MQTT error:', err.message));
|
|
91
|
+
client.on('offline', () => console.warn('[Yoyo ije] MQTT offline — will retry'));
|
|
92
|
+
}
|
|
93
|
+
disconnect() {
|
|
94
|
+
this.client?.end(true);
|
|
95
|
+
this.client = null;
|
|
96
|
+
}
|
|
97
|
+
brokerSubscribe(topic) {
|
|
98
|
+
this.client?.subscribe(topic, { qos: 0 }, err => {
|
|
99
|
+
if (err) {
|
|
100
|
+
console.error(`[Yoyo ije] Failed to subscribe to ${topic}:`, err.message);
|
|
101
|
+
}
|
|
102
|
+
else if (this.debug) {
|
|
103
|
+
console.log(`[Yoyo ije][MQTT] subscribed ✓ ${topic}`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Device payloads are JSON. The backend wraps device data in a single-element
|
|
108
|
+
// array ([{...}]); unwrap it so widgets always receive a plain object.
|
|
109
|
+
parsePayload(message) {
|
|
110
|
+
let text;
|
|
111
|
+
try {
|
|
112
|
+
text = new TextDecoder().decode(message);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (!text)
|
|
118
|
+
return null;
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(text);
|
|
121
|
+
const obj = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
122
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
123
|
+
return obj;
|
|
124
|
+
}
|
|
125
|
+
console.warn('[Yoyo ije] Ignoring non-object MQTT payload');
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
console.warn('[Yoyo ije] Ignoring malformed MQTT payload');
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SdkConfig } from './index';
|
|
2
|
+
export interface ChatChartSpec {
|
|
3
|
+
chart_type: 'bar' | 'line' | 'pie' | 'scatter' | 'table';
|
|
4
|
+
title: string;
|
|
5
|
+
labels: string[];
|
|
6
|
+
datasets: Array<{
|
|
7
|
+
label: string;
|
|
8
|
+
data: number[];
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export interface ChatResponse {
|
|
12
|
+
session_id: string;
|
|
13
|
+
answer: string;
|
|
14
|
+
chart?: ChatChartSpec;
|
|
15
|
+
}
|
|
16
|
+
export declare class IjeChatClient {
|
|
17
|
+
private sessionId;
|
|
18
|
+
private config;
|
|
19
|
+
_setConfig(config: SdkConfig): void;
|
|
20
|
+
ask(question: string): Promise<ChatResponse>;
|
|
21
|
+
resetSession(): void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class IjeChatClient {
|
|
2
|
+
sessionId = null;
|
|
3
|
+
config = null;
|
|
4
|
+
_setConfig(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
async ask(question) {
|
|
8
|
+
if (!this.config) {
|
|
9
|
+
throw new Error('[Yoyo ije] SDK must be initialized before using chat.');
|
|
10
|
+
}
|
|
11
|
+
const response = await fetch(`${this.config.apiUrl}/api/v1/apigateway/mimir/insights/query`, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
Authorization: `Bearer ${this.config.token}`,
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
session_id: this.sessionId,
|
|
19
|
+
question,
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`[Yoyo ije] Chat query failed: ${response.status}`);
|
|
24
|
+
}
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
this.sessionId = data.session_id;
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
resetSession() {
|
|
30
|
+
this.sessionId = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IjeMqttManager } from './mqttManager';
|
|
2
|
+
import { IjeChatClient } from './chatClient';
|
|
3
|
+
export type { ChatChartSpec, ChatResponse } from './chatClient';
|
|
4
|
+
export interface SdkConfig {
|
|
5
|
+
token: string;
|
|
6
|
+
theme?: {
|
|
7
|
+
primaryColor?: string;
|
|
8
|
+
fontFamily?: string;
|
|
9
|
+
borderRadius?: string;
|
|
10
|
+
};
|
|
11
|
+
apiUrl?: string;
|
|
12
|
+
mqttUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class IjeSDK {
|
|
15
|
+
private static instance;
|
|
16
|
+
config: SdkConfig | null;
|
|
17
|
+
isInitialized: boolean;
|
|
18
|
+
mqtt: IjeMqttManager;
|
|
19
|
+
chat: IjeChatClient;
|
|
20
|
+
private constructor();
|
|
21
|
+
static getInstance(): IjeSDK;
|
|
22
|
+
init(config: SdkConfig): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Access the Ije SDK by Yoyo
|
|
26
|
+
*/
|
|
27
|
+
export declare const Ije: IjeSDK;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { IjeMqttManager } from './mqttManager';
|
|
2
|
+
import { IjeChatClient } from './chatClient';
|
|
3
|
+
export class IjeSDK {
|
|
4
|
+
static instance;
|
|
5
|
+
config = null;
|
|
6
|
+
isInitialized = false;
|
|
7
|
+
mqtt;
|
|
8
|
+
chat;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.mqtt = new IjeMqttManager();
|
|
11
|
+
this.chat = new IjeChatClient();
|
|
12
|
+
}
|
|
13
|
+
static getInstance() {
|
|
14
|
+
if (!IjeSDK.instance) {
|
|
15
|
+
IjeSDK.instance = new IjeSDK();
|
|
16
|
+
}
|
|
17
|
+
return IjeSDK.instance;
|
|
18
|
+
}
|
|
19
|
+
async init(config) {
|
|
20
|
+
if (this.isInitialized) {
|
|
21
|
+
console.warn('[Yoyo ije] SDK is already initialized');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.config = {
|
|
25
|
+
apiUrl: 'https://api.yoyomq.com',
|
|
26
|
+
mqttUrl: 'wss://mqtt.yoyomq.com',
|
|
27
|
+
...config,
|
|
28
|
+
};
|
|
29
|
+
this.chat._setConfig(this.config);
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(`${this.config.apiUrl}/api/v1/sdk/authenticate`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
Authorization: `Bearer ${config.token}`,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
this.mqtt.connect(this.config.mqttUrl, data.mqttToken);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// MQTT is best-effort — insights chat works without it.
|
|
45
|
+
}
|
|
46
|
+
if (this.config.theme && typeof document !== 'undefined') {
|
|
47
|
+
const root = document.documentElement;
|
|
48
|
+
if (this.config.theme.primaryColor) {
|
|
49
|
+
root.style.setProperty('--yoyo-primary', this.config.theme.primaryColor);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.isInitialized = true;
|
|
53
|
+
console.log('[Yoyo ije] SDK initialized');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Access the Ije SDK by Yoyo
|
|
58
|
+
*/
|
|
59
|
+
export const Ije = IjeSDK.getInstance();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type MessageHandler = (payload: Record<string, any>) => void;
|
|
2
|
+
export declare class IjeMqttManager {
|
|
3
|
+
readonly subscriptions: Map<string, Set<MessageHandler>>;
|
|
4
|
+
subscribe(topic: string, handler: MessageHandler): void;
|
|
5
|
+
unsubscribe(topic: string, handler: MessageHandler): void;
|
|
6
|
+
dispatch(topic: string, payload: Record<string, any>): void;
|
|
7
|
+
connect(url: string, token: string): void;
|
|
8
|
+
}
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class IjeMqttManager {
|
|
2
|
+
// Keyed by topic — components subscribe/unsubscribe via these Sets
|
|
3
|
+
subscriptions = new Map();
|
|
4
|
+
subscribe(topic, handler) {
|
|
5
|
+
if (!this.subscriptions.has(topic)) {
|
|
6
|
+
this.subscriptions.set(topic, new Set());
|
|
7
|
+
}
|
|
8
|
+
this.subscriptions.get(topic).add(handler);
|
|
9
|
+
}
|
|
10
|
+
unsubscribe(topic, handler) {
|
|
11
|
+
this.subscriptions.get(topic)?.delete(handler);
|
|
12
|
+
}
|
|
13
|
+
// Dispatches a message to all subscribers of a topic — used by the real MQTT
|
|
14
|
+
// client and by the demo mock loop to inject synthetic payloads.
|
|
15
|
+
dispatch(topic, payload) {
|
|
16
|
+
this.subscriptions.get(topic)?.forEach(h => h(payload));
|
|
17
|
+
}
|
|
18
|
+
connect(url, token) {
|
|
19
|
+
// Real MQTT-over-WebSocket connection — to be implemented
|
|
20
|
+
console.log(`[Yoyo ije] MQTT ready (${url})`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { SdkConfig } from './index';
|
|
2
|
+
/** A user-created Trigger and what it aggregates (public view). */
|
|
3
|
+
export interface IjeTrigger {
|
|
4
|
+
id: number;
|
|
5
|
+
uuid: string;
|
|
6
|
+
name: string;
|
|
7
|
+
status: boolean;
|
|
8
|
+
events: string[];
|
|
9
|
+
aggregators: {
|
|
10
|
+
key: string;
|
|
11
|
+
data_key: string | null;
|
|
12
|
+
}[];
|
|
13
|
+
created_at: string;
|
|
14
|
+
updated_at: string;
|
|
15
|
+
}
|
|
16
|
+
/** A Device the API key's Organization owns (only the fields the SDK relies on are typed). */
|
|
17
|
+
export interface IjeDevice {
|
|
18
|
+
device_id: number;
|
|
19
|
+
name: string;
|
|
20
|
+
identifier: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
/** Lightweight aggregated-event list item: identity + window + whether it carries a route. */
|
|
24
|
+
export interface IjeAggregatedEvent {
|
|
25
|
+
id: number;
|
|
26
|
+
event_group_id: number;
|
|
27
|
+
device_id: number;
|
|
28
|
+
trigger_id: number;
|
|
29
|
+
msg_start_time: string;
|
|
30
|
+
msg_end_time: string;
|
|
31
|
+
has_route: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** One aggregated event with its full message_content. */
|
|
34
|
+
export interface IjeAggregatedEventDetail {
|
|
35
|
+
id: number;
|
|
36
|
+
event_group_id: number;
|
|
37
|
+
device_id: number;
|
|
38
|
+
trigger_id: number;
|
|
39
|
+
msg_start_time: string;
|
|
40
|
+
msg_end_time: string;
|
|
41
|
+
message_content: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
/** One raw telemetry row. `data` holds the device payload (lat/lng live here). */
|
|
44
|
+
export interface IjeDeviceDataPoint {
|
|
45
|
+
id: number;
|
|
46
|
+
device_id: number;
|
|
47
|
+
message_timestamp: string;
|
|
48
|
+
server_timestamp: string;
|
|
49
|
+
data: Record<string, any>;
|
|
50
|
+
created_at: string;
|
|
51
|
+
}
|
|
52
|
+
export interface IjeTriggersResponse {
|
|
53
|
+
triggers: IjeTrigger[];
|
|
54
|
+
total: number;
|
|
55
|
+
}
|
|
56
|
+
export interface IjeDevicesResponse {
|
|
57
|
+
devices: IjeDevice[];
|
|
58
|
+
total: number;
|
|
59
|
+
}
|
|
60
|
+
export interface IjeAggregatedEventsResponse {
|
|
61
|
+
aggregated_events: IjeAggregatedEvent[];
|
|
62
|
+
total: number;
|
|
63
|
+
}
|
|
64
|
+
export interface IjeDeviceDataResponse {
|
|
65
|
+
data: IjeDeviceDataPoint[];
|
|
66
|
+
total: number;
|
|
67
|
+
limit: number;
|
|
68
|
+
offset: number;
|
|
69
|
+
}
|
|
70
|
+
export interface ListAggregatedEventsParams {
|
|
71
|
+
triggerId: number;
|
|
72
|
+
deviceIds?: number[];
|
|
73
|
+
/** Window start, Unix seconds (filters msg_start_time). */
|
|
74
|
+
startsAt?: number;
|
|
75
|
+
/** Window end, Unix seconds (filters msg_start_time). */
|
|
76
|
+
endsAt?: number;
|
|
77
|
+
hasRoute?: boolean;
|
|
78
|
+
sortOrder?: 'ASC' | 'DESC';
|
|
79
|
+
limit?: number;
|
|
80
|
+
offset?: number;
|
|
81
|
+
}
|
|
82
|
+
export interface GetDeviceDataParams {
|
|
83
|
+
deviceIds?: number[];
|
|
84
|
+
/** JSONB expression for filtering the data field (e.g. "timestamp >= 1764234113000 AND timestamp <= 1764236012000"). Timestamps must be in milliseconds. */
|
|
85
|
+
partialQueryExpression?: string;
|
|
86
|
+
order?: 'ASC' | 'DESC';
|
|
87
|
+
limit?: number;
|
|
88
|
+
offset?: number;
|
|
89
|
+
}
|
|
90
|
+
export declare class IjeTripsClient {
|
|
91
|
+
private http;
|
|
92
|
+
private config;
|
|
93
|
+
_setConfig(config: SdkConfig): void;
|
|
94
|
+
listTriggers(params?: {
|
|
95
|
+
searchText?: string;
|
|
96
|
+
limit?: number;
|
|
97
|
+
offset?: number;
|
|
98
|
+
deviceId?: number;
|
|
99
|
+
}): Promise<IjeTriggersResponse>;
|
|
100
|
+
listDevices(params?: {
|
|
101
|
+
searchText?: string;
|
|
102
|
+
limit?: number;
|
|
103
|
+
offset?: number;
|
|
104
|
+
}): Promise<IjeDevicesResponse>;
|
|
105
|
+
listAggregatedEvents(params: ListAggregatedEventsParams): Promise<IjeAggregatedEventsResponse>;
|
|
106
|
+
getAggregatedEvent(id: number): Promise<IjeAggregatedEventDetail>;
|
|
107
|
+
getDeviceData(params: GetDeviceDataParams): Promise<IjeDeviceDataResponse>;
|
|
108
|
+
/**
|
|
109
|
+
* Fetches all telemetry for a window by paging through device_data and returns
|
|
110
|
+
* chronological [lng, lat] pairs ready for maplibre.
|
|
111
|
+
* startsAt and endsAt are Unix milliseconds.
|
|
112
|
+
*/
|
|
113
|
+
getTripPath(params: {
|
|
114
|
+
deviceIds: number[];
|
|
115
|
+
startsAt: number;
|
|
116
|
+
endsAt: number;
|
|
117
|
+
}): Promise<[number, number][]>;
|
|
118
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { IjeHttpClient } from './httpClient';
|
|
2
|
+
export class IjeTripsClient {
|
|
3
|
+
http = new IjeHttpClient();
|
|
4
|
+
config = null;
|
|
5
|
+
_setConfig(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.http._setConfig(config);
|
|
8
|
+
}
|
|
9
|
+
listTriggers(params = {}) {
|
|
10
|
+
return this.http.get('/public/api/v1/triggers', {
|
|
11
|
+
params: { searchText: params.searchText, limit: params.limit, offset: params.offset, deviceId: params.deviceId },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
listDevices(params = {}) {
|
|
15
|
+
return this.http.get('/public/api/v1/devices', {
|
|
16
|
+
params: { searchText: params.searchText, limit: params.limit, offset: params.offset },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
listAggregatedEvents(params) {
|
|
20
|
+
return this.http.get('/public/api/v1/aggregated_events', {
|
|
21
|
+
params: {
|
|
22
|
+
trigger_id: params.triggerId,
|
|
23
|
+
starts_at: params.startsAt,
|
|
24
|
+
ends_at: params.endsAt,
|
|
25
|
+
has_route: params.hasRoute,
|
|
26
|
+
sort_order: params.sortOrder,
|
|
27
|
+
limit: params.limit,
|
|
28
|
+
offset: params.offset,
|
|
29
|
+
},
|
|
30
|
+
arrayParams: params.deviceIds?.length ? { 'device_ids[]': params.deviceIds } : undefined,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
getAggregatedEvent(id) {
|
|
34
|
+
return this.http.get(`/public/api/v1/aggregated_events/${id}`);
|
|
35
|
+
}
|
|
36
|
+
getDeviceData(params) {
|
|
37
|
+
return this.http.get('/public/api/v1/device_data', {
|
|
38
|
+
params: {
|
|
39
|
+
partial_query_expression: params.partialQueryExpression,
|
|
40
|
+
order: params.order,
|
|
41
|
+
limit: params.limit,
|
|
42
|
+
offset: params.offset,
|
|
43
|
+
},
|
|
44
|
+
arrayParams: params.deviceIds?.length ? { 'device_ids[]': params.deviceIds } : undefined,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Fetches all telemetry for a window by paging through device_data and returns
|
|
49
|
+
* chronological [lng, lat] pairs ready for maplibre.
|
|
50
|
+
* startsAt and endsAt are Unix milliseconds.
|
|
51
|
+
*/
|
|
52
|
+
async getTripPath(params) {
|
|
53
|
+
const debug = this.config?.debug;
|
|
54
|
+
const pageSize = 500;
|
|
55
|
+
const path = [];
|
|
56
|
+
const partialQueryExpression = `timestamp >= ${params.startsAt} AND timestamp <= ${params.endsAt}`;
|
|
57
|
+
let totalRows = 0;
|
|
58
|
+
let validCoords = 0;
|
|
59
|
+
for (let offset = 0;; offset += pageSize) {
|
|
60
|
+
const page = await this.getDeviceData({
|
|
61
|
+
deviceIds: params.deviceIds,
|
|
62
|
+
partialQueryExpression,
|
|
63
|
+
order: 'ASC',
|
|
64
|
+
limit: pageSize,
|
|
65
|
+
offset,
|
|
66
|
+
});
|
|
67
|
+
totalRows += page.data.length;
|
|
68
|
+
if (debug && offset === 0 && page.data.length > 0) {
|
|
69
|
+
console.log('[Yoyo ije][TripPath] first row data sample:', page.data[0].data);
|
|
70
|
+
}
|
|
71
|
+
for (const point of page.data) {
|
|
72
|
+
const coordinate = extractLngLat(point);
|
|
73
|
+
if (coordinate) {
|
|
74
|
+
path.push(coordinate);
|
|
75
|
+
validCoords++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (page.data.length < pageSize)
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (debug) {
|
|
82
|
+
console.log(`[Yoyo ije][TripPath] fetched ${totalRows} rows → ${validCoords} valid coords → path length ${path.length}`, { partialQueryExpression });
|
|
83
|
+
}
|
|
84
|
+
return path;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Reads a [lng, lat] pair from a telemetry row, tolerating common field name variants. */
|
|
88
|
+
function extractLngLat(point) {
|
|
89
|
+
const data = point?.data ?? {};
|
|
90
|
+
const lat = Number(data.lat ?? data.latitude ?? data.Lat ?? data.Latitude);
|
|
91
|
+
const lng = Number(data.lng ?? data.lon ?? data.longitude ?? data.Lng ?? data.Lon ?? data.Longitude);
|
|
92
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lng))
|
|
93
|
+
return null;
|
|
94
|
+
if (lat < -90 || lat > 90 || lng < -180 || lng > 180)
|
|
95
|
+
return null;
|
|
96
|
+
return [lng, lat];
|
|
97
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yoyomq/ije-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"mqtt": "^5.14.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "latest"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "node_modules/.bin/tsc",
|
|
21
|
+
"dev": "node_modules/.bin/tsc --watch"
|
|
22
|
+
}
|
|
23
|
+
}
|