ncube-thyme-typescript
Version:
nCube Thyme for Typescript (oneM2M client)
916 lines (881 loc) • 25.4 kB
text/typescript
import Chalk from "chalk"
import Debug from "debug"
import shortid from "shortid"
import { TypedEmitter } from "tiny-typed-emitter"
import { M2M_AE, M2M_AERes } from "./onem2m/m2m_ae"
import { convertM2MTimestamp, M2MBase, M2MError } from "./onem2m/m2m_base"
import { M2M_CB, M2M_CBRes } from "./onem2m/m2m_cb"
import { M2M_CIN, M2M_CINRes } from "./onem2m/m2m_cin"
import { M2M_CNT, M2M_CNTRes } from "./onem2m/m2m_cnt"
import { resNotExists } from "./onem2m/m2m_debug"
import {
CINSubCallback,
HTTPTransport,
M2MHeader,
M2MKeys,
M2MOperation,
M2MSubTransport,
M2MTransport,
MQTTTransport,
} from "./onem2m/m2m_protocol"
import { M2MStatusCode } from "./onem2m/m2m_rsp"
import { M2M_SUB, M2M_SUBRes } from "./onem2m/m2m_sub"
import { M2MType } from "./onem2m/m2m_type"
const debugMain = Debug("Thyme_main:main")
const debugSub = Debug("Thyme_main:sub")
const defaultMaxBufferSize = 20480
const APIVER = "0.2.481.2.0001.001.000111"
const m2m_ae_basebody = {
api: APIVER,
lbl: ["key1", "key2"],
rr: true,
}
export class Thyme<M extends ThymeProtocol, S extends SubscribeProtos>
implements ThymeBase
{
protected mainProtoName: M
protected subProtoName: S
public mainProtocol: M2MTransport
public subProtocol: M2MSubTransport
public baseM2MHeader: M2MHeader
public constructor(options: Thyme2Option<M, S>) {
this.mainProtoName = options.main.type
this.subProtoName = (options.sub?.type ?? this.mainProtoName) as S
this.baseM2MHeader = {
"X-M2M-RI": `thyme/${shortid()}`,
}
const getProto = (name: "main" | "sub") => {
const opt = options[name]
switch (opt.type) {
case ThymeProtocol.HTTP:
return new HTTPTransport(
opt.host ?? options.main.host,
opt.port,
opt.secure ?? false
)
break
case ThymeProtocol.MQTT:
return new MQTTTransport(
opt.host ?? options.main.host,
opt.port,
opt.secure ?? false
)
break
}
throw new Error("Not implemented")
}
this.mainProtocol = getProto("main")
if (options.sub == null) {
this.subProtocol = this.mainProtocol as M2MSubTransport
} else {
this.subProtocol = getProto("sub") as M2MSubTransport
}
}
public async connect() {
if (this.mainProtoName === (this.subProtoName as ThymeProtocol)) {
const connected = await this.mainProtocol.connect()
if (!connected) {
throw new Error("Socket isn't connected.")
}
} else {
const connected =
(await this.mainProtocol.connect()) &&
(await this.subProtocol.connect())
if (!connected) {
throw new Error("Socket isn't connected.")
}
}
}
/**
* Get CommonServiceEntity Instance.
*
* `cseId` is required for mqtt protocol!
* @param cseName
* @param cseId
* @returns
*/
public async getCSEBase(cseName: string, cseId?: string) {
const cseBaseRes = await this.mainProtocol.request<M2M_CBRes>({
opcode: M2MOperation.RETRIEVE,
rootCSE: {
name: cseName,
id: cseId ?? "",
},
params: [],
useReg: true,
header: {
...this.baseM2MHeader,
"X-M2M-Origin": `S`,
},
})
const cseBase = cseBaseRes.response[M2MKeys.cseBase]
if (cseBase.rn != cseName) {
throw new Error(`CSEBase does not match to ${cseName}!`)
}
debugMain(`${Chalk.gray("CSEBase")} ${Chalk.redBright(cseBase.rn)} found.`)
return new ThymeCSE(this, cseBase)
}
}
/**
* Thyme CSE(Common Service Entry)
*/
export class ThymeCSE implements CSEBase, ThymeBase {
public readonly type = M2MType.CSEBase
public readonly createdTime: Date
public readonly modifiedTime: Date
public readonly resourceName: string
public readonly resourceId: string
public readonly cseName: string
public readonly cseId: string
public readonly raw: M2M_CB
protected pointOfAccess: string[] = []
public readonly mainProtocol: M2MTransport
public readonly subProtocol: M2MSubTransport
public readonly baseM2MHeader: M2MHeader
public constructor(thymeBase: ThymeBase, resp: M2M_CB) {
this.mainProtocol = thymeBase.mainProtocol
this.subProtocol = thymeBase.subProtocol
this.baseM2MHeader = thymeBase.baseM2MHeader
this.raw = resp
this.createdTime = convertM2MTimestamp(resp.ct)
this.modifiedTime = convertM2MTimestamp(resp.lt)
this.resourceName = resp.rn
this.resourceId = resp.ri
this.cseName = this.resourceName
this.cseId = resp.csi.startsWith("/") ? resp.csi.substring(1) : resp.csi
this.pointOfAccess.push(...resp.poa)
}
public async connect() {
// nothing
return true
}
/**
* Create AE(Application Entity).
*
* If `ae` is exists, it will throw error.
*
* @param resourceName AE name
* @returns Created ApplicationEntity object
* @throws `Error` if `ae` is exist
*/
public async createApplicationEntity(resourceName: string) {
const createAERes = await this.mainProtocol.request<M2M_AERes>({
opcode: M2MOperation.CREATE,
resType: M2MType.ApplicationEntity,
rootCSE: {
name: this.cseName,
id: this.cseId,
},
params: [],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": `S${resourceName}`,
},
body: {
"m2m:ae": {
...m2m_ae_basebody,
poa: this.pointOfAccess,
rn: resourceName,
},
},
})
const response = createAERes.response[M2MKeys.applicationEntity]
const out: ApplicationEntity = this.respToSerial(response)
debugMain(
`${getDeclaredName(out)} with id ${Chalk.yellow(
response.aei
)} has been ${Chalk.greenBright("created")}.`
)
return out
}
/**
* Delete `AE`(Application Entity).
*
* If `ae` is not exists, it will throw error.
*
* @param resource `AE` name or `AE` object
* @returns Deleted `AE` object. (Do not use it)
* @throws `Error` if `ae` isn't exist
*/
public async deleteApplicationEntity(
resource: string | ApplicationEntity
): Promise<ApplicationEntity> {
const resName =
typeof resource === "string" ? resource : resource.resourceName
// const aeInfo = await this.queryApplicationEntity(resName)
const deleteAERes = await this.mainProtocol.request<M2M_AERes>({
opcode: M2MOperation.DELETE,
rootCSE: {
name: this.cseName,
id: this.cseId,
},
params: [resName],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": "Superman",
},
})
const response = deleteAERes.response[M2MKeys.applicationEntity]
const out: ApplicationEntity = this.respToSerial(response)
debugMain(
`${getDeclaredName(out)} with id ${Chalk.yellow(
response.aei
)} has been ${Chalk.redBright("removed")}.`
)
return out
}
/**
* Query `AE`(Application Entity) and return `AE` info.
* @param resourceName `AE` name
* @returns Queried `AE` object
* @throws `Error` if `AE` isn't exist
*/
public async queryApplicationEntity(
resourceName: string
): Promise<ApplicationEntity> {
const queryAERes = await this.mainProtocol.request<M2M_AERes>({
opcode: M2MOperation.RETRIEVE,
rootCSE: {
name: this.cseName,
id: this.cseId,
},
params: [resourceName],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": `S${resourceName}`,
},
})
const response = queryAERes.response[M2MKeys.applicationEntity]
return this.respToSerial(response)
}
/**
* Ensure given `AE`(Application Entity) is exist without error.
* @param resourceName `AE` name
* @param cleanup if enabled, `AE` is cleaned up if exists.
* @returns Created or queried `AE` with active state
*/
public async ensureApplicationEntity(
resourceName: string,
cleanup?: boolean
): Promise<ThymeAE> {
let ae: ApplicationEntity = null
try {
ae = await this.queryApplicationEntity(resourceName)
} catch (err: unknown) {
if (err instanceof M2MError) {
if (err.responseCode === M2MStatusCode.NOT_FOUND) {
// pass
} else {
throw err
}
} else {
throw err
}
}
if (ae != null) {
if (cleanup != null && cleanup) {
await this.deleteApplicationEntity(ae)
} else {
return this.connectAE(ae)
}
}
// create
return this.connectAE(await this.createApplicationEntity(resourceName))
}
/**
* Activate ApplicationEntity with subscribe & command
* @param ae ApplicationEntity object
* @returns ThymeAE
*/
public async connectAE(ae: ApplicationEntity) {
const out = new ThymeAE(this, ae)
await out.connect()
return out
}
/**
* Response to serialized object
* @param resp Response
* @returns Serialized Object
*/
protected respToSerial(resp: M2M_AE): ApplicationEntity {
return {
type: M2MType.ApplicationEntity,
...serialGeneral(resp),
parentCSE: this,
aei: resp.aei,
raw: resp,
}
}
}
/**
* Thyme AE(Application Entity)
*/
export class ThymeAE implements ApplicationEntity, ThymeBase {
/* predefine */
public readonly type: M2MType.ApplicationEntity = M2MType.ApplicationEntity
public readonly resourceName: string
public readonly resourceId: string
public readonly createdTime: Date
public readonly modifiedTime: Date
public readonly parentCSE: ThymeCSE
public readonly raw: M2M_AE
public readonly mainProtocol: M2MTransport
public readonly subProtocol: M2MSubTransport
public readonly baseM2MHeader: M2MHeader
public readonly aei: string
public constructor(parentCSE: ThymeCSE, ae: ApplicationEntity) {
this.aei = ae.aei
this.resourceName = ae.resourceName
this.resourceId = ae.resourceId
this.createdTime = ae.createdTime
this.modifiedTime = ae.modifiedTime
this.parentCSE = parentCSE
this.raw = ae.raw
// CSE
this.mainProtocol = parentCSE.mainProtocol
this.subProtocol = parentCSE.subProtocol
this.baseM2MHeader = parentCSE.baseM2MHeader
}
public async connect() {
// @todo mqtt watch
}
/**
* Create Container in `ae`
* @param containerName Container Name
* @param maxBufferSize **the max size of state. (in bytes)**
* @returns Created Container
* @throws If given name container is exist.
*/
public async createContainer(
containerName: string,
maxBufferSize?: number
): Promise<Container> {
const createCntRes = await this.mainProtocol.request<M2M_CNTRes>({
opcode: M2MOperation.CREATE,
resType: M2MType.Container,
rootCSE: {
name: this.parentCSE.cseName,
id: this.parentCSE.cseId,
},
params: [this.resourceName],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": this.aei,
},
body: {
"m2m:cnt": {
rn: containerName,
lbl: [containerName],
mbs: maxBufferSize ?? defaultMaxBufferSize, // maxBufferSize
},
},
})
const response = createCntRes.response[M2MKeys.container]
const out: Container = this.respToSerial(response)
debugMain(
`${getDeclaredName(out)} has been ${Chalk.greenBright("created")}.`
)
return out
}
/**
* Delete Container in `ae` or just container
* @param container Container name or Container
* @returns Deleted `Container` object. (Do not use it)
* @throws If given `Container` isn't exist
*/
public async deleteContainer(
container: Container | string
): Promise<Container> {
const conName =
typeof container === "string" ? container : container.resourceName
const deleteCntRes = await this.mainProtocol.request<M2M_CNTRes>({
opcode: M2MOperation.DELETE,
rootCSE: {
name: this.parentCSE.cseName,
id: this.parentCSE.cseId,
},
params: [this.resourceName, conName],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": this.aei,
},
})
const response = deleteCntRes.response[M2MKeys.container]
const out: Container = this.respToSerial(response)
debugMain(`${getDeclaredName(out)} has been ${Chalk.redBright("removed")}.`)
return out
}
/**
* Query `CNT`(Container) and return Container info.
* @param containerName Container Name
* @returns Queried Container object
* @throws `Error` if `AE` or Container isn't exist
*/
public async queryContainer(containerName: string): Promise<Container> {
const queryCntRes = await this.mainProtocol.request<M2M_AERes>({
opcode: M2MOperation.RETRIEVE,
rootCSE: {
name: this.parentCSE.cseName,
id: this.parentCSE.cseId,
},
params: [this.resourceName, containerName],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": this.aei,
},
})
const response = queryCntRes.response[M2MKeys.container]
return this.respToSerial(response)
}
/**
* Ensure given container is exist
* @param containerName Container Name to query
* @param maxBufferSize max Buffer size of value
* @param cleanup the bool of Clear container
* @returns Container of `containerName`
*/
public async ensureContainer(
containerName: string,
maxBufferSize: number,
cleanup?: boolean
) {
let container: Container = null
try {
container = await this.queryContainer(containerName)
} catch (err: unknown) {
if (err instanceof M2MError) {
if (err.responseCode === M2MStatusCode.NOT_FOUND) {
// pass
} else {
throw err
}
} else {
throw err
}
}
if (container != null) {
if (cleanup != null && cleanup) {
await this.deleteContainer(container)
} else {
const cont = new ThymeContainer(this, container)
await cont.connect()
return cont
}
}
// create
const cont = new ThymeContainer(
this,
await this.createContainer(containerName, maxBufferSize)
)
await cont.connect()
return cont
}
protected respToSerial(resp: M2M_CNT): Container {
return {
type: M2MType.Container,
...serialGeneral(resp),
parentAE: this, // immutable..?
maxBufferSize: resp.mbs,
raw: resp,
}
}
}
interface ThymeContainerEvents {
changed: (value: string, cin: ContentInstance) => unknown
}
/**
* Container
*
* or sensor value store?
*/
export class ThymeContainer
extends TypedEmitter<ThymeContainerEvents>
implements Container, ThymeBase
{
/* predefine */
public readonly type = M2MType.Container
public readonly resourceName: string
public readonly resourceId: string
public readonly createdTime: Date
public readonly modifiedTime: Date
public readonly raw: M2M_CNT
public readonly parentAE: ThymeAE
public readonly mainProtocol: M2MTransport
public readonly subProtocol: M2MSubTransport
public readonly baseM2MHeader: M2MHeader
public readonly maxBufferSize: number
public constructor(parentAE: ThymeAE, container: Container) {
super()
this.resourceName = container.resourceName
this.resourceId = container.resourceId
this.createdTime = container.createdTime
this.modifiedTime = container.modifiedTime
this.parentAE = parentAE
this.raw = container.raw
// CSE
this.mainProtocol = parentAE.mainProtocol
this.subProtocol = parentAE.subProtocol
this.baseM2MHeader = parentAE.baseM2MHeader
// Value
this.maxBufferSize = container.maxBufferSize
}
public async connect() {
await this.subscribe("thyme_cin_sub", (raw: M2M_CIN) => {
const cin = this.respToSerial(raw)
debugSub(
`${getDeclaredName(cin)} has changed to ${Chalk.blueBright(cin.value)}.`
)
this.emit("changed", cin.value, cin)
})
return true
}
/**
* Add ContentInstance value to Container
*
* aka. Put sensor value
* @param value Value (string)
* @returns ContentInstance
*/
public async addContentInstance(value: string): Promise<ContentInstance> {
const ae = this.parentAE
const createCinRes = await this.mainProtocol.request<M2M_CINRes>({
opcode: M2MOperation.CREATE,
resType: M2MType.ContentInstance,
rootCSE: {
name: ae.parentCSE.cseName,
id: ae.parentCSE.cseId,
},
params: [ae.resourceName, this.resourceName],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": ae.aei,
},
body: {
"m2m:cin": {
con: value,
},
},
})
const response = createCinRes.response[M2MKeys.contentInstance]
const out: ContentInstance = this.respToSerial(response)
debugMain(
`${getDeclaredName(out)} has been updated to ${Chalk.cyanBright(
response.con
)}.`
)
return out
}
/**
* Get latest value of data
*
* If data isn't presented, It will return `null`
* @returns string value(sensor data)
*/
public async queryLastValue() {
try {
const ae = this.parentAE
const queryCinRes = await this.mainProtocol.request<M2M_CINRes>({
opcode: M2MOperation.RETRIEVE,
rootCSE: {
name: ae.parentCSE.cseName,
id: ae.parentCSE.cseId,
},
params: [ae.resourceName, this.resourceName, "la"],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": ae.aei,
},
})
const value = queryCinRes.response["m2m:cin"].con
return value
} catch (err: unknown) {
if (err instanceof M2MError) {
if (err.responseCode === M2MStatusCode.NOT_FOUND) {
// pass
} else {
throw err
}
} else {
throw err
}
}
return null
}
/**
* Query ContentInstance
*
* Uses filter
* @param filter Filter (before, after, latest cound)
* @returns last `value` (string)
* @throws There isn't any ContentInstance value
*/
public async queryContentInstance(filter: {
before?: Date
after?: Date
latest: number
}): Promise<ContentInstance[]> {
const ae = this.parentAE
const queryCinRes = await this.mainProtocol.request<{
"m2m:rsp": { "m2m:cin": M2M_CIN[] }
}>({
opcode: M2MOperation.RETRIEVE,
rootCSE: {
name: ae.parentCSE.cseName,
id: ae.parentCSE.cseId,
},
params: [ae.resourceName, this.resourceName],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": ae.aei,
},
urlOptions: {
ty: M2MType.ContentInstance, // resource type
rcn: M2MType.ContentInstance, // response "content type"
fu: 2, // API version? unknown
crb: convertM2MTimestamp(filter.before) ?? undefined,
cra: convertM2MTimestamp(filter.after) ?? undefined,
la: filter.latest,
},
})
const response = queryCinRes.response["m2m:rsp"]["m2m:cin"]
const out: ContentInstance[] = response.map((v) => this.respToSerial(v))
debugMain(
`${getDeclaredName(out[0])} is ${out
.map((v) => Chalk.blueBright(v.value))
.join("|")}.`
)
return out
}
/**
* Subscribe container.
* @param container Container
* @param subscribeName subscribe name
* @returns subscribe data?
*/
public async subscribe(
subscribeName: string,
callback: CINSubCallback
): Promise<SubscribeData> {
const ae = this.parentAE
let response: M2M_SUB
try {
const subExistRes = await this.mainProtocol.request<M2M_SUBRes>({
opcode: M2MOperation.RETRIEVE,
rootCSE: {
name: ae.parentCSE.cseName,
id: ae.parentCSE.cseId,
},
params: [ae.resourceName, this.resourceName, subscribeName],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": ae.aei,
},
})
response = subExistRes.response[M2MKeys.subscribe]
} catch (err) {
if (err instanceof M2MError && err.debugLog === resNotExists) {
const createSubRes = await this.mainProtocol.request<M2M_SUBRes>({
opcode: M2MOperation.CREATE,
resType: M2MType.Subscribe,
rootCSE: {
name: ae.parentCSE.cseName,
id: ae.parentCSE.cseId,
},
params: [ae.resourceName, this.resourceName],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": ae.aei,
},
body: {
"m2m:sub": {
rn: subscribeName,
enc: {
net: [1, 2, 3, 4],
},
nu: [`${this.subProtocol.getBaseURL()}/${ae.aei}?ct=json`],
exc: 10,
},
},
})
response = createSubRes.response[M2MKeys.subscribe]
} else {
throw err
}
}
// also subscribe from subscribe protocol
await this.subProtocol.subscribeContainer(this, subscribeName, callback)
debugSub(
`${getDeclaredName(this)} is now ${Chalk.greenBright("subscribed")}.`
)
return {
type: M2MType.Subscribe,
...serialGeneral(response),
raw: response,
container: this,
notiURL: response.nu[0],
}
}
public async unsubscribe(subscribeName: string): Promise<SubscribeData> {
const ae = this.parentAE
const deleteCntRes = await this.mainProtocol.request<M2M_SUBRes>({
opcode: M2MOperation.DELETE,
rootCSE: {
name: ae.parentCSE.cseName,
id: ae.parentCSE.cseId,
},
params: [ae.resourceName, this.resourceName, subscribeName],
header: {
...this.baseM2MHeader,
"X-M2M-Origin": ae.aei,
},
})
const response = deleteCntRes.response[M2MKeys.subscribe]
// also unsubscribe from protocol
await this.subProtocol.unsubscribeContainer(this, subscribeName)
debugSub(
`${getDeclaredName(this)} is now ${Chalk.redBright("unsubscribed")}.`
)
return {
type: M2MType.Subscribe,
...serialGeneral(response),
raw: response,
container: this,
notiURL: response.nu[0],
}
}
protected respToSerial(resp: M2M_CIN): ContentInstance {
return {
type: M2MType.ContentInstance,
...serialGeneral(resp),
container: this,
value: resp.con,
raw: resp,
}
}
}
interface Resource {
type: M2MType
resourceName: string
resourceId: string
createdTime: Date
modifiedTime: Date
}
export interface CSEBase extends Readonly<Resource> {
readonly type: M2MType.CSEBase
readonly raw: M2M_CB
readonly cseName: string
readonly cseId: string // for mqtt
}
export interface ApplicationEntity extends Readonly<Resource> {
readonly type: M2MType.ApplicationEntity
readonly parentCSE: CSEBase
readonly raw: M2M_AE
/**
* Application Entity Id
*/
readonly aei: string
}
export interface Container extends Readonly<Resource> {
readonly type: M2MType.Container
readonly parentAE: ApplicationEntity
readonly raw: M2M_CNT
/**
* Max value size (length)
*/
readonly maxBufferSize: number
}
export interface ContentInstance extends Readonly<Resource> {
readonly type: M2MType.ContentInstance
readonly raw: M2M_CIN
readonly container: Container
readonly value: string
}
export interface SubscribeData extends Readonly<Resource> {
readonly type: M2MType.Subscribe
readonly container: Container
readonly raw: M2M_SUB
readonly notiURL: string
}
/**
* Support protocols
*/
type SubscribeProtos = ThymeProtocol.WebSocket | ThymeProtocol.MQTT
export type Thyme2Option<
T extends ThymeProtocol,
V extends SubscribeProtos | void
> = {
main: {
type: T
host: string
port: number
secure?: boolean
}
} & (T extends SubscribeProtos
? {
sub?: {
type: V
port: number
secure?: boolean
host?: string
}
}
: {
sub: {
type: V
port: number
secure?: boolean
host?: string
}
})
export enum ThymeProtocol {
HTTP = "http",
MQTT = "mqtt",
WebSocket = "webSocket",
CoAP = "coap",
}
interface ThymeBase {
mainProtocol: M2MTransport
subProtocol: M2MSubTransport
baseM2MHeader: M2MHeader
connect: () => Promise<unknown>
}
export interface ThymeOption {
host: string
main: "http" // @todo support mqtt main
http: {
port: number
secure?: false
}
mqtt: {
port: number
}
}
/**
* Print name of element
* @param entity Entity
* @returns beautiful output
*/
function getDeclaredName(
entity: ApplicationEntity | Container | ContentInstance
) {
if (entity.type === M2MType.ApplicationEntity) {
return `${Chalk.blue(entity.resourceName)}${Chalk.gray("(AE)")}`
} else if (entity.type === M2MType.Container) {
return `${Chalk.green(entity.resourceName)}${Chalk.gray(
"(CNT)"
)} in ${getDeclaredName(entity.parentAE)}`
} else if (entity.type === M2MType.ContentInstance) {
return `The value of ${getDeclaredName(entity.container)}`
} else {
return `unknown`
}
}
function serialGeneral<T extends M2MBase>(resp: T) {
return {
createdTime: convertM2MTimestamp(resp.ct),
modifiedTime: convertM2MTimestamp(resp.lt),
resourceName: resp.rn,
resourceId: resp.ri,
raw: resp,
}
}