fostrom 0.0.5 → 0.0.7

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.
Files changed (3) hide show
  1. package/index.js +289 -0
  2. package/package.json +5 -2
  3. package/readme.md +42 -0
package/index.js CHANGED
@@ -0,0 +1,289 @@
1
+ import { execSync } from "child_process"
2
+ import { fileURLToPath } from "url"
3
+ import path from "path"
4
+
5
+ function agent_path() {
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ return path.join(__dirname, ".agent", "fostrom-device-agent")
8
+ }
9
+
10
+ class FostromError extends Error {
11
+ constructor(atom, msg) {
12
+ const formattedMessage = `\x1b[31m\x1b[1mFostrom Error\x1b[0m \x1b[34m[${atom}]\x1b[0m ${msg}`
13
+ super(formattedMessage)
14
+ this.name = ""
15
+ this.stack = null
16
+ }
17
+ }
18
+
19
+ export class Mail {
20
+ id
21
+ name
22
+ payload = undefined
23
+ mailbox_size
24
+ #instance
25
+
26
+ constructor(fostrom_instance, id, name, payload, mailbox_size) {
27
+ this.#instance = fostrom_instance
28
+ this.id = id
29
+ this.name = name
30
+ this.payload = payload
31
+ this.mailbox_size = mailbox_size
32
+ }
33
+
34
+ async ack() { return await this.#instance.__mail_op("ack", this.id) }
35
+ async reject() { return await this.#instance.__mail_op("reject", this.id) }
36
+ async requeue() { return await this.#instance.__mail_op("requeue", this.id) }
37
+ }
38
+
39
+ export default class Fostrom {
40
+ #log = true
41
+ #creds = {}
42
+
43
+ onMail = async mail => {
44
+ console.log(`[Fostrom] Received Mail (Mailbox Size: ${mail.mailbox_size}): ${mail.name} -> ID ${mail.id}`)
45
+ console.warn(" Auto-Acknowledging Mail. Define Mail Handler to handle incoming mail.\n `fostrom.on_mail = async (mail) => { ...; await mail.ack(); }`\n")
46
+ await mail.ack()
47
+ }
48
+
49
+ connected = () => {
50
+ if (this.#log) console.log("[Fostrom] Connected")
51
+ }
52
+
53
+ unauthorized = (reason, after) => {
54
+ if (this.#log) {
55
+ const after_s = Math.floor(after / 1000);
56
+ console.log(`[Fostrom] Unauthorized: ${reason}. Reconnecting in ${after_s} seconds...`)
57
+ }
58
+ }
59
+
60
+ reconnecting = (reason, after) => {
61
+ if (this.#log) {
62
+ const after_s = Math.floor(after / 1000);
63
+ console.log(`[Fostrom] Failed to connect: ${reason}. Reconnecting in ${after_s} seconds...`)
64
+ }
65
+ }
66
+
67
+ constructor(config = {}) {
68
+ if (!config.fleet_id) throw "[Fostrom] Fleet ID required."
69
+ if (!config.device_id) throw "[Fostrom] Device ID required."
70
+ if (!config.device_secret) throw "[Fostrom] Device Secret required."
71
+ this.#creds.fleet_id = config.fleet_id
72
+ this.#creds.device_id = config.device_id
73
+ this.#creds.device_secret = config.device_secret
74
+ if (config.log == false) this.#log = false
75
+ }
76
+
77
+ #start_agent() {
78
+ const { fleet_id, device_id, device_secret } = this.#creds
79
+
80
+ let args = [
81
+ "start",
82
+ "--fleet-id", fleet_id,
83
+ "--device-id", device_id,
84
+ "--device-secret", device_secret
85
+ ]
86
+
87
+ if (process.env.FOSTROM_AGENT_MODE === 'dev') args.push("--dev")
88
+
89
+ try {
90
+ const output = execSync(`${agent_path()} ${args.join(" ")}`, { encoding: "utf8" })
91
+
92
+ if (output.trim() == "starting") {
93
+ execSync(`sleep 0.25`)
94
+ return
95
+ }
96
+ } catch (error) {
97
+ let [err, msg] = error.stdout.trim().split(":", 2)
98
+ throw new FostromError(err, msg)
99
+ }
100
+ }
101
+
102
+ async #req(path = "/", method = "GET", payload = null, additional_options = {}) {
103
+ if (method != "GET" && method != "PUT" && method != "POST" && method != "DELETE") {
104
+ throw new Error(`Unsupported ${method} Request for path ${path}`)
105
+ }
106
+
107
+ const headers = new Headers()
108
+ headers.set("Content-Type", "application/json")
109
+ headers.set("Accept", "application/json")
110
+
111
+ const base_options = {
112
+ method,
113
+ headers,
114
+ }
115
+
116
+ if ((method == "POST" || method == "PUT") && payload) {
117
+ base_options.body = JSON.stringify(payload)
118
+ }
119
+
120
+ const options = {
121
+ ...base_options,
122
+ ...additional_options
123
+ }
124
+
125
+ let resp
126
+ let body
127
+
128
+ try {
129
+ resp = await fetch(`http://localhost:8585${path}`, options)
130
+ } catch (e) {
131
+ throw new FostromError("req_failed", "Communicating with the device agent is failing.")
132
+ }
133
+
134
+ const fleet_id = resp.headers.get('x-fleet-id')
135
+ const device_id = resp.headers.get('x-device-id')
136
+
137
+ if (this.#creds.fleet_id != fleet_id || this.#creds.device_id != device_id) {
138
+ console.warn("Restarting Agent due to credential mismatch.")
139
+ this.stop_agent()
140
+ this.#start_agent()
141
+ return await this.#req(path, options)
142
+ } else {
143
+ try {
144
+ body = await resp.json()
145
+ }
146
+ catch (e) {
147
+ console.error(e)
148
+ throw new FostromError("req_failed", "Communicating with the device agent is failing.")
149
+ }
150
+ }
151
+
152
+ if (body.error && body.msg) {
153
+ throw new FostromError(body.error, body.msg)
154
+ } else {
155
+ return body
156
+ }
157
+ }
158
+
159
+ static stopAgent() {
160
+ try {
161
+ execSync(`${agent_path()} stop`, { encoding: "utf8" })
162
+ } catch (e) {
163
+ console.error("[Fostrom] Failed to stop the Fostrom Device Agent")
164
+ }
165
+ }
166
+
167
+ async connect() {
168
+ this.#start_agent()
169
+ const resp = await this.#req()
170
+ const status = resp.connect_status
171
+
172
+ if (!status.connected) {
173
+ console.error(new FostromError(status.error, status.msg))
174
+ } else {
175
+ this.#open_event_stream()
176
+ return true
177
+ }
178
+ }
179
+
180
+ async sendDatapoint(name, payload) {
181
+ return await this.#req(`/pulse/datapoint/${name}`, "POST", payload)
182
+ }
183
+
184
+ async sendMsg(name, payload) {
185
+ return await this.#req(`/pulse/msg/${name}`, "POST", payload)
186
+ }
187
+
188
+ async mailboxStatus() {
189
+ const resp = await this.#req(`/mailbox/status`)
190
+
191
+ if (resp.mailbox_empty == true) {
192
+ return {
193
+ mailbox_size: 0,
194
+ next_mail_id: null,
195
+ next_mail_name: null
196
+ }
197
+ } else {
198
+ return {
199
+ mailbox_size: resp.mailbox_size,
200
+ next_mail_id: resp.pulse_id,
201
+ next_mail_name: resp.name,
202
+ }
203
+ }
204
+ }
205
+
206
+ async nextMail() {
207
+ const resp = await this.#req(`/mailbox/next`)
208
+
209
+ if (resp.mailbox_empty == true) {
210
+ return null
211
+ }
212
+
213
+ return new Mail(this, resp.pulse_id, resp.name, resp.payload, resp.mailbox_size)
214
+ }
215
+
216
+ async __mail_op(op, mail_id) {
217
+ if (op != "ack" && op != "reject" && op != "requeue") {
218
+ throw new Error("Invalid Mailbox Operation")
219
+ }
220
+
221
+ return await this.#req(`/mailbox/${op}/${mail_id}`, "PUT")
222
+ }
223
+
224
+ async #event_handler(event_object) {
225
+ const { event, _timestamp } = event_object
226
+ const data = event_object.data
227
+
228
+ switch (event) {
229
+ case 'connected': this.connected(); break;
230
+ case 'unauthorized': this.unauthorized(data.reason, data.reconnecting_in); break;
231
+ case 'connect_failed': this.reconnecting(data.reason, data.reconnecting_in); break;
232
+ case 'next_mail': this.#dispatch_next_mail(data); break;
233
+ default: null
234
+ }
235
+ }
236
+
237
+ async #dispatch_next_mail(data) {
238
+ const mail = new Mail(this, data.pulse_id, data.name, data.payload, data.mailbox_size)
239
+ this.onMail(mail)
240
+ }
241
+
242
+ async #open_event_stream() {
243
+ const td = new TextDecoder()
244
+
245
+ const resp = await fetch(`http://localhost:8585/events`, {
246
+ headers: { 'Accept': 'text/event-stream' }
247
+ })
248
+
249
+ const reader = resp.body.getReader()
250
+ let buffer = ''
251
+
252
+ while (true) {
253
+ const { done, value } = await reader.read()
254
+ if (done) { this.connect() }
255
+ const decoded = td.decode(value)
256
+ parse_events(buffer, decoded, this.#event_handler.bind(this))
257
+ }
258
+ }
259
+ }
260
+
261
+ function parse_events(buffer, chunk, event_handler) {
262
+ buffer += chunk
263
+ const lines = buffer.split('\n')
264
+
265
+ // Keep the last incomplete line in buffer
266
+ buffer = lines.pop() || ''
267
+
268
+ let event = {}
269
+
270
+ for (const line of lines) {
271
+ // Empty line indicates end of event
272
+ if (line === '') {
273
+ if (event.data && event.data != '') {
274
+ event.data = JSON.parse(event.data)
275
+ }
276
+
277
+ event_handler(event)
278
+ event = {}
279
+ } else if (line.startsWith('data: ')) {
280
+ event.data = (event.data || '') + line.slice(6)
281
+ } else if (line.startsWith('event: ')) {
282
+ event.event = line.slice(7)
283
+ } else if (line.startsWith('id: ')) {
284
+ event.timestamp = new Date(parseInt(line.slice(4)))
285
+ } else if (line.startsWith('retry: ')) {
286
+ event.retry = parseInt(line.slice(7))
287
+ }
288
+ }
289
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fostrom",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Fostrom's Official Device SDK for JS. Fostrom (https://fostrom.io) is an IoT Cloud Platform.",
5
5
  "keywords": [
6
6
  "fostrom",
@@ -22,8 +22,11 @@
22
22
  "author": "Fostrom <support@fostrom.io> (https://fostrom.io)",
23
23
  "files": [
24
24
  "index.js",
25
- "dl-agent.sh"
25
+ "dl-agent.sh",
26
+ "readme.md"
26
27
  ],
28
+ "main": "index.js",
29
+ "type": "module",
27
30
  "scripts": {
28
31
  "postinstall": "sh dl-agent.sh .agent"
29
32
  },
package/readme.md ADDED
@@ -0,0 +1,42 @@
1
+ # Fostrom Device SDK
2
+
3
+ [Fostrom](https://fostrom.io) is an IoT Cloud Platform built for developers. Monitor and control your fleet of devices, from microcontrollers to industrial IoT. Designed to be simple, secure, and fast. Experience first-class tooling with Device SDKs, type-safe schemas, programmable actions, and more.
4
+
5
+ The Fostrom Device SDK for JavaScript works in Node.js and Bun, on Linux and macOS, and helps you quickly integrate, start monitoring, and controlling your IoT devices in just a few lines of code.
6
+
7
+ ## Example
8
+
9
+ ```js
10
+ import Fostrom from 'fostrom';
11
+
12
+ const fostrom = new Fostrom({
13
+ fleet_id: "<fleet-id>",
14
+ device_id: "<device-id>",
15
+ device_secret: "<device-secret>",
16
+ })
17
+
18
+ // Setup the on_mail handler, to process incoming mail.
19
+ fostrom.onMail = async (mail) => {
20
+ {id, name, payload, mailbox_size} = mail
21
+ console.debug(`Received Mail (${mailbox_size}): ${name} ${id}`)
22
+ await mail.ack()
23
+ }
24
+
25
+ async function main() {
26
+ await fostrom.connect()
27
+
28
+ // To send a message to Fostrom
29
+ await fostrom.sendMsg("<packet-schema-name>", { ...payload })
30
+
31
+ // To send a datapoint to Fostrom
32
+ await fostrom.sendDatapoint("<packet-schema-name>", { ...payload })
33
+ }
34
+
35
+ main()
36
+ ```
37
+
38
+ ## A Note on the Device Agent
39
+
40
+ The Fostrom Device SDK downloads and runs the Fostrom Device Agent in the background. The Agent is downloaded when the package is installed through `npm`. The Device Agent is started when `fostrom.connect()` is called, and it remains running in the background forever.
41
+
42
+ We recommend you allow the Device Agent to run continously, even if your program has exited or crashed, so that when your program is automatically restarted by a process manager, the reconnection to Fostrom is nearly instant. However, if you wish to stop the Device Agent, you can call `Fostrom.stopAgent()` in your code before terminating your program.