├── .gitignore ├── lerna.json ├── tsconfig.json ├── packages ├── module-publish │ ├── package.json │ ├── src │ │ └── index.ts │ └── dist │ │ ├── module-publish.esm.js │ │ ├── module-publish.cjs.js │ │ └── module-publish.umd.js ├── module-default-codecs │ ├── package.json │ ├── src │ │ └── index.ts │ └── dist │ │ ├── module-default-codecs.esm.js │ │ ├── module-default-codecs.cjs.js │ │ └── module-default-codecs.umd.js ├── module-espruino-networking │ ├── package.json │ ├── dist │ │ ├── module-espruino-networking.esm.js │ │ ├── module-espruino-networking.cjs.js │ │ └── module-espruino-networking.umd.js │ └── src │ │ └── index.ts └── core │ ├── package.json │ ├── src │ ├── Module.ts │ └── index.ts │ ├── dist │ ├── core.esm.js │ ├── core.cjs.js │ └── core.umd.js │ └── package-lock.json ├── package.json ├── rollup.config.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent" 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "paths": { 10 | "@lightnub/*": ["./packages/*/src/index.ts"] 11 | } 12 | }, 13 | "include": ["packages/**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/module-publish/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lightnub/module-publish", 3 | "version": "1.0.0", 4 | "description": "LightNub dx plugin implementing publish", 5 | "author": "Artur Wojciechowski ", 6 | "license": "MIT", 7 | "main": "dist/module-publish.cjs.js", 8 | "module": "dist/module-publish.esm.js", 9 | "browser": "dist/module-publish.umd.js", 10 | "files": [ 11 | "dist" 12 | ], 13 | "publishConfig": { 14 | "access": "public" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/module-default-codecs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lightnub/module-default-codecs", 3 | "version": "1.0.0", 4 | "description": "LightNub codec plugin for ES5 JavaScript environments", 5 | "author": "Artur Wojciechowski ", 6 | "license": "MIT", 7 | "main": "dist/module-default-codecs.cjs.js", 8 | "module": "dist/module-default-codecs.esm.js", 9 | "browser": "dist/module-default-codecs.umd.js", 10 | "files": [ 11 | "dist" 12 | ], 13 | "publishConfig": { 14 | "access": "public" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/module-espruino-networking/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lightnub/module-espruino-networking", 3 | "version": "1.0.0", 4 | "description": "LightNub networking plugin for Espruino boards", 5 | "author": "Artur Wojciechowski ", 6 | "license": "MIT", 7 | "main": "dist/module-espruino-networking.cjs.js", 8 | "module": "dist/module-espruino-networking.esm.js", 9 | "browser": "dist/module-espruino-networking.umd.js", 10 | "files": [ 11 | "dist" 12 | ], 13 | "publishConfig": { 14 | "access": "public" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lightnub/core", 3 | "version": "1.0.0", 4 | "description": "A lightweight alternative to PubNub JavaScript SDK specifically created for IoT devices", 5 | "author": "Artur Wojciechowski ", 6 | "license": "MIT", 7 | "main": "dist/core.cjs.js", 8 | "module": "dist/core.esm.js", 9 | "browser": "dist/core.umd.js", 10 | "files": [ 11 | "dist" 12 | ], 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "dependencies": { 17 | "narrows": "^3.0.2", 18 | "shallowequal": "^1.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "devDependencies": { 5 | "@rollup/plugin-commonjs": "^21.0.1", 6 | "@rollup/plugin-json": "^4.1.0", 7 | "@rollup/plugin-node-resolve": "^13.1.3", 8 | "@rollup/plugin-typescript": "^8.3.0", 9 | "@types/shallowequal": "^1.1.1", 10 | "lerna": "^4.0.0", 11 | "prettier": "^2.5.1", 12 | "rollup": "^2.66.1", 13 | "rollup-plugin-terser": "^7.0.2", 14 | "tslib": "^2.3.1", 15 | "typescript": "^4.5.5" 16 | }, 17 | "prettier": { 18 | "semi": false, 19 | "printWidth": 120, 20 | "singleQuote": true 21 | }, 22 | "scripts": { 23 | "build": "lerna exec -- rollup -c ../../rollup.config.js" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import typescript from '@rollup/plugin-typescript' 4 | import json from '@rollup/plugin-json' 5 | 6 | import { terser } from 'rollup-plugin-terser' 7 | 8 | const PACKAGE_ROOT_PATH = process.cwd() 9 | const { LERNA_PACKAGE_NAME, LERNA_ROOT_PATH } = process.env 10 | 11 | const packageName = LERNA_PACKAGE_NAME.split('/')[1] 12 | 13 | export default { 14 | input: `${PACKAGE_ROOT_PATH}/src/index.ts`, 15 | output: [ 16 | { 17 | file: `dist/${packageName}.cjs.js`, 18 | format: 'cjs', 19 | exports: 'default', 20 | }, 21 | { 22 | file: `dist/${packageName}.esm.js`, 23 | format: 'esm', 24 | }, 25 | { 26 | name: packageName, 27 | file: `dist/${packageName}.umd.js`, 28 | format: 'umd', 29 | }, 30 | ], 31 | plugins: [json(), resolve(), commonjs(), typescript({ tsconfig: `${LERNA_ROOT_PATH}/tsconfig.json` }), terser()], 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/Module.ts: -------------------------------------------------------------------------------- 1 | import type { Core } from '.' 2 | 3 | export type ModuleInterfaceType = 'codec' | 'networking' | 'dx' 4 | 5 | type CodecModuleInterface = { 6 | encode(input: T): string 7 | decode(input: string): T 8 | } 9 | 10 | export type Request = { 11 | path: string 12 | method: string 13 | headers?: Record 14 | body?: string 15 | } 16 | 17 | export type Response = { 18 | status: number 19 | body: string 20 | headers: Record 21 | } 22 | 23 | type NetworkingModuleInterface = { 24 | request(request: Request): Promise 25 | } 26 | 27 | type EmptyModuleInterface = void 28 | 29 | export type KnownModuleInterfaces = { 30 | codec: CodecModuleInterface 31 | networking: NetworkingModuleInterface 32 | dx: EmptyModuleInterface 33 | } 34 | 35 | export interface Module { 36 | name: string 37 | version: string 38 | type: T 39 | 40 | filter?: Record 41 | 42 | initialize?(core: Core): void 43 | 44 | get(): KnownModuleInterfaces[T] 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/dist/core.esm.js: -------------------------------------------------------------------------------- 1 | var t=function(){function t(){this.modules=[]}return t.prototype.registerModule=function(t){var e;this.modules.push(t),null===(e=t.initialize)||void 0===e||e.call(t,this)},t.prototype.get=function(t,e){var r=this.modules.find((function(r){return r.type===t&&(void 0===r.filter||void 0===e||function(t,e,r,n){var i=r?r.call(n,t,e):void 0;if(void 0!==i)return!!i;if(t===e)return!0;if("object"!=typeof t||!t||"object"!=typeof e||!e)return!1;var o=Object.keys(t),u=Object.keys(e);if(o.length!==u.length)return!1;for(var l=Object.prototype.hasOwnProperty.bind(e),f=0;f { 5 | version = version 6 | name = 'default-codec-json' 7 | type = 'codec' as const 8 | 9 | filter = { format: 'json' } 10 | 11 | get() { 12 | return this 13 | } 14 | 15 | encode(input: any): string { 16 | return JSON.stringify(input) 17 | } 18 | 19 | decode(input: string): any { 20 | return JSON.parse(input) 21 | } 22 | } 23 | 24 | class UrlCodec implements Module<'codec'> { 25 | version = version 26 | name = 'default-codec-url' 27 | type = 'codec' as const 28 | 29 | filter = { format: 'url' } 30 | 31 | get() { 32 | return this 33 | } 34 | 35 | encode(input: any): string { 36 | return encodeURIComponent(input) 37 | } 38 | 39 | decode(input: string): any { 40 | return decodeURIComponent(input) 41 | } 42 | } 43 | 44 | export default function WithDefaultCodec(klazz: T) { 45 | return class WithEspruinoNetworking extends klazz { 46 | constructor(...args: any[]) { 47 | super(...args) 48 | 49 | this.registerModule(new JsonCodec()) 50 | this.registerModule(new UrlCodec()) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/dist/core.umd.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).core=t()}(this,(function(){"use strict";var e=function(){function e(){this.modules=[]}return e.prototype.registerModule=function(e){var t;this.modules.push(e),null===(t=e.initialize)||void 0===t||t.call(e,this)},e.prototype.get=function(e,t){var n=this.modules.find((function(n){return n.type===e&&(void 0===n.filter||void 0===t||function(e,t,n,i){var r=n?n.call(i,e,t):void 0;if(void 0!==r)return!!r;if(e===t)return!0;if("object"!=typeof e||!e||"object"!=typeof t||!t)return!1;var o=Object.keys(e),u=Object.keys(t);if(o.length!==u.length)return!1;for(var f=Object.prototype.hasOwnProperty.bind(t),l=0;l { 5 | version = version 6 | name = 'publish-dx' 7 | type = 'dx' as const 8 | 9 | get() {} 10 | } 11 | 12 | type PublishParams = { 13 | channel: string 14 | message: any 15 | keyset: Keyset 16 | } 17 | 18 | export default function WithPublish(klazz: T) { 19 | return class WithPublish extends klazz { 20 | constructor(...args: any[]) { 21 | super(...args) 22 | 23 | this.registerModule(new PublishModule()) 24 | } 25 | 26 | publish({ channel, message, keyset }: PublishParams) { 27 | const networking = this.get('networking') 28 | const jsonCodec = this.get('codec', { format: 'json' }) 29 | const urlCodec = this.get('codec', { format: 'url' }) 30 | 31 | const encodedMessage = urlCodec.encode(jsonCodec.encode(message)) 32 | const encodedChannel = urlCodec.encode(channel) 33 | 34 | return networking 35 | .request({ 36 | method: 'get', 37 | path: `/publish/${keyset.publishKey}/${keyset.subscribeKey}/0/${encodedChannel}/0/${encodedMessage}`, 38 | }) 39 | .then((response) => { 40 | return jsonCodec.decode(response.body) 41 | }) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lightnub/core", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@lightnub/core", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "narrows": "^3.0.2", 13 | "shallowequal": "^1.1.0" 14 | } 15 | }, 16 | "node_modules/narrows": { 17 | "version": "3.0.2", 18 | "resolved": "https://registry.npmjs.org/narrows/-/narrows-3.0.2.tgz", 19 | "integrity": "sha512-sTda0nh/PYKZ7KYLzEkDcvSzl/kfVKXO0PGPHqU32mfyy2//zmsdJxuql9TbP6sMdE1SryJs3hOAgYlYozUkOw==" 20 | }, 21 | "node_modules/shallowequal": { 22 | "version": "1.1.0", 23 | "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", 24 | "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" 25 | } 26 | }, 27 | "dependencies": { 28 | "narrows": { 29 | "version": "3.0.2", 30 | "resolved": "https://registry.npmjs.org/narrows/-/narrows-3.0.2.tgz", 31 | "integrity": "sha512-sTda0nh/PYKZ7KYLzEkDcvSzl/kfVKXO0PGPHqU32mfyy2//zmsdJxuql9TbP6sMdE1SryJs3hOAgYlYozUkOw==" 32 | }, 33 | "shallowequal": { 34 | "version": "1.1.0", 35 | "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", 36 | "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import shallowequal from 'shallowequal' 2 | 3 | import type { Module, ModuleInterfaceType, KnownModuleInterfaces } from './Module' 4 | 5 | import { version } from '../package.json' 6 | 7 | export * from './Module' 8 | export interface Keyset { 9 | subscribeKey: string 10 | uuid: string 11 | 12 | publishKey?: string 13 | } 14 | 15 | export type CoreType = { new (...args: any[]): Core } 16 | class Core { 17 | static version = version 18 | 19 | private modules: Module[] = [] 20 | 21 | protected registerModule(module: Module) { 22 | this.modules.push(module) 23 | 24 | module.initialize?.(this) 25 | } 26 | 27 | protected get(type: T, filter?: Record): KnownModuleInterfaces[T] { 28 | const module = this.modules.find( 29 | (module) => 30 | module.type === type && 31 | (module.filter === undefined || filter === undefined || shallowequal(module.filter, filter)) 32 | ) as Module 33 | 34 | if (!module) { 35 | throw new Error( 36 | `A ${type} module is missing that can fulfill following requirements: ${JSON.stringify( 37 | filter 38 | )}. Make sure to include this module during instantiation.` 39 | ) 40 | } 41 | 42 | return module.get() 43 | } 44 | } 45 | 46 | export type { Core } 47 | 48 | // @ts-ignore 49 | export default function LightNub(...decorators) { 50 | // @ts-ignore 51 | return new (decorators.reduce((klass, decorator) => decorator(klass), Core))() 52 | } 53 | 54 | LightNub.Core = Core 55 | -------------------------------------------------------------------------------- /packages/module-publish/dist/module-publish.esm.js: -------------------------------------------------------------------------------- 1 | /*! ***************************************************************************** 2 | Copyright (c) Microsoft Corporation. 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 9 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | ***************************************************************************** */ 15 | var t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n])},t(e,n)};var e=function(){function t(){this.version="1.0.0",this.name="publish-dx",this.type="dx"}return t.prototype.get=function(){},t}();function n(n){return function(n){function o(){for(var t=[],o=0;o void) => void 6 | end: (body?: string) => void 7 | } 8 | 9 | type HttpResponse = { 10 | on: (eventName: 'data' | 'close', callback: (data?: string) => void) => void 11 | statusCode: number 12 | headers: { [key: string]: string } 13 | } 14 | 15 | type HttpRequestOptions = { 16 | host: string 17 | path: string 18 | method: string 19 | port?: number 20 | protocol?: 'http:' | 'https:' 21 | headers?: { [key: string]: string } 22 | } 23 | 24 | type HttpModule = { 25 | request(options: HttpRequestOptions, callback: (response: HttpResponse) => void): HttpRequest 26 | } 27 | 28 | class EspruinoNetworking implements Module<'networking'> { 29 | version = version 30 | name = 'espruino-networking' 31 | type = 'networking' as const 32 | 33 | httpModule: HttpModule 34 | 35 | constructor(httpModule: HttpModule) { 36 | this.httpModule = httpModule 37 | } 38 | 39 | get() { 40 | return this 41 | } 42 | 43 | request(request: Request): Promise { 44 | return new Promise((resolve, reject) => { 45 | const req = this.httpModule.request( 46 | { 47 | host: 'ps.pndsn.com', 48 | method: request.method.toUpperCase(), 49 | path: request.path, 50 | headers: request.headers, 51 | }, 52 | (res) => { 53 | let result = '' 54 | 55 | res.on('data', (data) => { 56 | result += data 57 | }) 58 | 59 | res.on('close', () => { 60 | resolve({ status: res.statusCode, body: result, headers: res.headers }) 61 | }) 62 | } 63 | ) 64 | 65 | req.on('error', (error) => { 66 | reject(error) 67 | }) 68 | 69 | if (request.body) { 70 | req.end(request.body) 71 | } else { 72 | req.end() 73 | } 74 | }) 75 | } 76 | } 77 | 78 | export default (httpModule: HttpModule) => 79 | function WithEspruinoNetworking(klazz: T) { 80 | return class WithEspruinoNetworking extends klazz { 81 | constructor(...args: any[]) { 82 | super(...args) 83 | 84 | this.registerModule(new EspruinoNetworking(httpModule)) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LightNub 2 | 3 | This is a Proof-of-Concept library that provides PubNub-compatible JavaScript SDK 4 | for IoT and embedded devices capable of running JavaScript. 5 | 6 | Minimal required ECMAScript version supported is ES5, but the code emitted should 7 | work on older implementations with correct polyfills. 8 | 9 | ## Usage 10 | 11 | This library is highly modularized, so you need to import multiple packages to get 12 | it working. 13 | 14 | Start by importing the `core` module, which provides the necessary structure 15 | for other modules to work. 16 | 17 | ```js 18 | const LightNub = require('@lightnub/core') 19 | ``` 20 | 21 | You need to provide additional modules to include specific features. 22 | Some modules are tagged as `networking` or `codec`, you need at least one of each 23 | for the SDK to work correctly. 24 | 25 | To instantiate the SDK with modules, you need to pass them as arguments to the `LightNub` function 26 | that you imported from `@lightnub/core`. 27 | 28 | ```js 29 | const LightNub = require('@lighnub/core') 30 | const Module1 = require('@lightnub/module-1') 31 | const Module2 = require('@lightnub/module-2') 32 | 33 | const pubnub = LightNub( 34 | Module1, 35 | Module2, 36 | ) 37 | 38 | // Now you can use pubnub 39 | pubnub.module1Function() 40 | pubnub.module2Function() 41 | ``` 42 | 43 | ## Modules 44 | ### `module-default-codecs` tags: codec 45 | This module provides basic `json` and `url` codecs for JavaScript environments 46 | that have `JSON.parse`, `JSON.stringify`, `decodeURIComponent` and `encodeURIComponent` 47 | available as globals. 48 | 49 | ```js 50 | const LightNub = require('@lightnub/core') 51 | const DefaultCodecs = require('@lightnub/module-default-codecs') 52 | 53 | const pubnub = LightNub( 54 | DefaultCodecs 55 | ) 56 | ``` 57 | 58 | ### `module-espruino-networking` tags: networking 59 | This module provides networking for IoT boards that run Espruino. 60 | You need to pass the `HttpModule` into the module function. 61 | 62 | ```js 63 | const http = require('http') 64 | const LightNub = require('@lightnub/core') 65 | const EspruinoNetworking = require('@lightnub/module-espruino-networking') 66 | 67 | const pubnub = LightNub( 68 | EspruinoNetworking(http) 69 | ) 70 | ``` 71 | 72 | ### `module-publish` requires: networking, codec 73 | This module provides the basic publish functionality. 74 | 75 | ```js 76 | const LightNub = require('@lightnub/core') 77 | const Networking = require('@lightnub/default-networking') 78 | const Codecs = require('@lightnub/default-codecs') 79 | 80 | const pubnub = LightNub( 81 | Networking, 82 | Codecs 83 | ) 84 | 85 | const keyset = { 86 | subscribeKey: 'demo', 87 | publishKey: 'demo', 88 | uuid: 'myUuid', 89 | } 90 | 91 | pubnub.publish({ message: { content: 'My message' }, channel: 'test', keyset: myKeyset }).then((response) => { 92 | console.log(response) 93 | }) 94 | ``` --------------------------------------------------------------------------------