Skip to content

Commit

Permalink
feat: handle shadow and updates
Browse files Browse the repository at this point in the history
  • Loading branch information
coderbyheart committed May 24, 2024
1 parent 19560d4 commit f6071ae
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 68 deletions.
28 changes: 15 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
"postinstall": "cp -r node_modules/svg-country-flags/svg/ static/flags"
},
"dependencies": {
"@hello.nrfcloud.com/proto": "10.0.0",
"@hello.nrfcloud.com/proto-map": "9.0.1",
"@hello.nrfcloud.com/proto": "12.0.1",
"@hello.nrfcloud.com/proto-map": "9.1.0",
"@sentry/browser": "^8.4.0",
"@sinclair/typebox": "0.32.31",
"classnames": "2.5.1",
Expand All @@ -51,7 +51,7 @@
"@aws-sdk/client-cloudfront": "3.583.0",
"@aws-sdk/client-iam": "3.583.0",
"@babel/plugin-syntax-import-assertions": "7.24.6",
"@bifravst/eslint-config-typescript": "6.0.26",
"@bifravst/eslint-config-typescript": "6.1.0",
"@bifravst/prettier-config": "1.0.0",
"@commitlint/config-conventional": "19.2.2",
"@nordicsemiconductor/cloudformation-helpers": "9.0.3",
Expand Down
76 changes: 55 additions & 21 deletions src/context/Device.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Context, DeviceIdentity } from '@hello.nrfcloud.com/proto/hello'
import {
Context,
type DeviceIdentity,
type LwM2MObjectUpdate,
type Shadow,
} from '@hello.nrfcloud.com/proto/hello'
import { type Static } from '@sinclair/typebox'
import { createContext, type ComponentChildren } from 'preact'
import {
Expand Down Expand Up @@ -92,30 +97,37 @@ export const Provider = ({ children }: { children: ComponentChildren }) => {
let message: any
try {
message = JSON.parse(msg.data)
const maybeValid = validPassthrough(message, (message, errors) => {
console.error(`[WS]`, `message dropped`, message, errors)
})
if (maybeValid !== null) {
console.debug(`[WS] <`, maybeValid)
setMessages((m) => [...m, message])
if (isDeviceIdentity(maybeValid)) {
const type = models[maybeValid.model] as Model
setDevice({
id: maybeValid.id,
lastSeen:
typeof maybeValid.lastSeen === 'string'
? new Date(maybeValid.lastSeen)
: undefined,
model: type,
})
setType(maybeValid.model)
}
listeners.current.map((listener) => listener(message))
}
} catch (err) {
console.error(`[WS]`, `Failed to parse message as JSON`, msg.data)
return
}
if (message === undefined) return
const maybeValid = validPassthrough(message, (message, errors) => {
console.error(`[WS]`, `message dropped`, message, errors)
})
if (maybeValid !== null) {
console.debug(`[WS] <`, maybeValid)
if (isDeviceIdentity(maybeValid)) {
const type = models[maybeValid.model] as Model
setDevice({
id: maybeValid.id,
lastSeen:
typeof maybeValid.lastSeen === 'string'
? new Date(maybeValid.lastSeen)
: undefined,
model: type,
})
setType(maybeValid.model)
} else if (isShadow(maybeValid)) {
const instances = maybeValid.reported.map(parseInstanceTimestamp)
setMessages((m) => [...m, ...instances])
listeners.current.forEach((listener) => instances.map(listener))
} else if (isUpdate(maybeValid)) {
const instance = parseInstanceTimestamp(maybeValid)
setMessages((m) => [...m, instance])
listeners.current.forEach((listener) => listener(instance))
}
}
}
ws.addEventListener('message', messageListener)

Expand Down Expand Up @@ -188,3 +200,25 @@ const isDeviceIdentity = (
isObject(message) &&
'@context' in message &&
message['@context'] === Context.deviceIdentity.toString()

const isShadow = (message: unknown): message is Static<typeof Shadow> =>
isObject(message) &&
'@context' in message &&
message['@context'] === Context.shadow.toString()

const isUpdate = (
message: unknown,
): message is Static<typeof LwM2MObjectUpdate> =>
isObject(message) &&
'@context' in message &&
message['@context'] === Context.lwm2mObjectUpdate.toString()

const parseInstanceTimestamp = (
i: LwM2MObjectInstance,
): LwM2MObjectInstance => ({
...i,
Resources: {
...i.Resources,
99: new Date(i.Resources[99] as number),
},
})
29 changes: 9 additions & 20 deletions src/proto/validPassthrough.spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,20 @@
import { validPassthrough } from './validPassthrough.js'
import { describe, test as it, mock } from 'node:test'
import assert from 'node:assert'
import { LwM2MObjectID } from '@hello.nrfcloud.com/proto-map/lwm2m'

void describe('validPassthrough', () => {
void it('should let valid input pass', () => {
const isValid = validPassthrough({
ObjectID: LwM2MObjectID.Geolocation_14201,
ObjectVersion: '1.0',
Resources: {
'0': 62.469414,
'1': 6.151946,
'6': 'Fixed',
'3': 1,
'99': new Date(1710147413003),
},
'@context': 'https://github.com/hello-nrfcloud/proto/deviceIdentity',
model: 'PCA20035+solar',
id: 'oob-352656108602296',
lastSeen: '2024-05-23T12:27:19.400Z',
})
assert.deepEqual(isValid, {
ObjectID: LwM2MObjectID.Geolocation_14201,
ObjectVersion: '1.0',
Resources: {
'0': 62.469414,
'1': 6.151946,
'6': 'Fixed',
'3': 1,
'99': new Date(1710147413003),
},
'@context': 'https://github.com/hello-nrfcloud/proto/deviceIdentity',
model: 'PCA20035+solar',
id: 'oob-352656108602296',
lastSeen: '2024-05-23T12:27:19.400Z',
})
})

Expand All @@ -37,6 +26,6 @@ void describe('validPassthrough', () => {
// input
assert.deepEqual(call?.arguments[0], { temp: -42 })
// Errors
assert.equal(call?.arguments[1] instanceof Error, true)
assert.equal(Array.isArray(call?.arguments[1]), true)
})
})
33 changes: 22 additions & 11 deletions src/proto/validPassthrough.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import { validateWithTypeBox } from '@hello.nrfcloud.com/proto'
import {
validate,
validators,
type LwM2MObjectInstance,
} from '@hello.nrfcloud.com/proto-map/lwm2m'
DeviceIdentity,
Shadow,
SingleCellGeoLocation,
LwM2MObjectUpdate,
} from '@hello.nrfcloud.com/proto/hello'
import { Type, type Static } from '@sinclair/typebox'
import type { ValueError } from '@sinclair/typebox/errors'

const validateInstance = validate(validators)
const IncomingMessage = Type.Union([
SingleCellGeoLocation,
DeviceIdentity,
Shadow,
LwM2MObjectUpdate,
])
type IncomingMessageType = Static<typeof IncomingMessage>
export const incomingMessageValidator = validateWithTypeBox(IncomingMessage)

export const validPassthrough = (
v: unknown,
onDropped?: (v: unknown, error: Error) => unknown,
): LwM2MObjectInstance | null => {
const maybeValidInstance = validateInstance(v)
if ('error' in maybeValidInstance) {
onDropped?.(v, maybeValidInstance.error)
onDropped?: (v: unknown, errors: Array<ValueError>) => unknown,
): IncomingMessageType | null => {
const maybeValidMessage = incomingMessageValidator(v)
if ('errors' in maybeValidMessage) {
onDropped?.(v, maybeValidMessage.errors)
return null
}
return maybeValidInstance.object
return maybeValidMessage.value
}

0 comments on commit f6071ae

Please sign in to comment.