diff --git a/README.md b/README.md index 34e30a4..5425915 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,105 @@ -# durable-stream-client -Client NodeJS application for `durable-stream` +# Durable Stream Client :electric_plug: + +A lightweight client for [Durable Stream](https://github.com/voxoco/durable-stream) + +The client + +## Installation + +```bash +npm install durable-stream-client +``` + +## Feaures +* Publish messages to a durable stream (to be propagated to other clients subscribed on the same subject) +* Simple reconnect logic +* Queues up messages while disconnected and sends them when reconnected (with a ~2 second timeout) +* Subscribe to messages on a durable stream +* Delete messages from a durable stream +* Get/set the object state (a generic object we can set/get on the durable object) +* Get metadata, get, put, delete objects in R2 + + +## Usage + +```js +// Shim the websocket client for node +globalThis.WebSocket = require('websocket').w3cwebsocket; + +import DurableStreamClient from 'durable-stream-client' + +const client = new DurableStreamClient({ + host: '.voxo.workers.dev', // defaults to localhost:8787 + secure: true, // boolean required (if local set to false) + apiKey: 'my-api-key', // string required + subject: 'my-subject', // string required +}) + +// Initialize the client +await client.init(); +``` + +## Primary Stream Methods + +```js +// Get the current sequence number and general stream info +const info = await client.info(); +console.log(info); + +const msg = { + test: 'value', + someData: 'some-data', +} + +// Publish a message (can be a string or object) +const res = await client.publish(msg) +// Returns a promise that resolves to the response from the server and includes the message id, sequence number etc.. + +// Subscribe to messages +// The first arg is the sequence number to start from (0 for all messages from the beginning of the stream) +client.subscribe(10000000019, async (msg, ack) => { + console.log(`Received message: ${JSON.stringify(msg)}`); + ack(); + // Be sure to ack all messages! + // Acknowledging a message will remove it from the queue on the client and server +}) + +// Unsubscribe from messages +await client.unsubscribe(); + +// Delete messages in the stream up to a sequence number +await client.delete(10000000019); + +// Get the object object state (just a generic object we can set/get on the durable object) +const state = await client.getState(); + +// Set the object state +await client.putState({ some: 'data' }); +``` + +## R2 Methods + +```js +// Head object (get metadata) +const metadata = await client.headObject('/path/to/object.ext'); + +// Get object +const object = await client.getObject('/path/to/object.ext'); +// Write the file to disk +fs.writeFileSync('/local/path/file.ext', object); + +// Put object +// Arg 1 = file path in R2 +// Arg 2 = local file path to upload + +const res = await client.putObject('/path/to/object.ext', '/local/path/file.ext'); + +// Delete object +const res = await client.deleteObject('/path/to/object.ext'); +``` + +## + +## License + +MIT \ No newline at end of file diff --git a/lib/main.js b/lib/main.js new file mode 100644 index 0000000..93bafc8 --- /dev/null +++ b/lib/main.js @@ -0,0 +1,212 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const axios_1 = __importDefault(require("axios")); +const fs = __importStar(require("fs")); +class DurableStreamClient { + constructor(obj) { + // Sanity check the api key + if (!obj.apiKey) + throw new Error('No apiKey exists'); + this.host = obj.host; + // Base url for R2 + this.apiKey = obj.apiKey; + this.r2Url = `${obj.secure ? 'https' : 'http'}://${obj.host}/r2`; + // Set the ws connection url + this.wsUrl = `${obj.secure ? 'wss' : 'ws'}://${obj.host}/stream/${obj.subject}?apiKey=${obj.apiKey}`; + this.ws = new WebSocket(this.wsUrl); + // Storing messages we are waiting for a response to + this.waitingOperations = {}; + // Message id counter and unique client id + const rand = Math.random(); + this.cMsgId = rand; + this.clientId = rand; + // Listener for messages being broadcasted from the server + this.listener = {}; + // Some state variables + this.isConnected = false; + this.reconnects = -1; + this.lastSequence = 0; + } + async init() { + while (true) { + this.isConnected = false; + // Break out if we are connected + if (this.ws.readyState === this.ws.OPEN) { + await new Promise(resolve => setTimeout(resolve, 1000)); + this.reconnects++; + this.isConnected = true; + console.log(`Connected to ${this.wsUrl} with ${this.reconnects} reconnects`); + // Re-establish listener if we have one + if (this.listener.doHandle) { + console.log(`Re-establishing listener at sequence ${this.lastSequence}`); + this.subscribe(this.lastSequence, this.listener.doHandle); + } + break; + } + // Reconnect if we are disconnected + if (this.ws.readyState === this.ws.CLOSED || this.ws.readyState === this.ws.CLOSING) { + console.log(`Attempting websocket connection to: ${this.host}`); + this.ws = new WebSocket(this.wsUrl); + } + // Run this function until we are connected + if (this.ws.readyState === this.ws.CONNECTING) { + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + // Setup listeners + await this.setupListeners(); + } + // Setup ws client listeners + async setupListeners() { + const closeHandler = async () => { + console.log('Session closed'); + await this.init(); + }; + this.ws.addEventListener("close", closeHandler); + this.ws.addEventListener("error", closeHandler); + // Listen for messages + this.ws.addEventListener('message', async (msg) => { + const json = JSON.parse(msg.data); + // Message from the server we should be subscribed to + if (json.pub) { + delete json.pub; + this.listener.doHandle(json, async () => { + this.lastSequence = json.sequence; + this.ws.send(JSON.stringify(json)); + }); + return; + } + // This must be a message from us to the server we are waiting for a response to + if (json.cMsgId) { + this.waitingOperations[json.cMsgId].resolveMe(json); + delete this.waitingOperations[json.cMsgId]; + return; + } + // Message from the server we need to respond to + if (json.sMsgId) { + this.ws.send(JSON.stringify(json)); + return; + } + }); + } + // Send a message to the websocket waiting for a response + async publish(msg) { + // Check can send + if (!await this.canSend()) + return { error: 'Could not send message' }; + return new Promise((resolve) => { + ++this.cMsgId; + this.waitingOperations[this.cMsgId] = { resolveMe: resolve, data: msg, cMsgId: this.cMsgId, clientId: this.clientId }; + this.ws.send(JSON.stringify({ data: msg, cMsgId: this.cMsgId, clientId: this.clientId })); + }); + } + // Check if we are able to send a message + async canSend() { + let tries = 0; + while (!this.isConnected) { + await new Promise(resolve => setTimeout(resolve, 500)); + tries++; + console.log(`Waiting for connection... Trying ${tries} out of 20 (2 seconds)`); + if (tries > 20) + break; + } + // If we have used up all our tries, return false + return tries < 20; + } + // Get info about the stream + async info() { + return await this.publish({ cmd: 'getStreamInfo' }); + } + // Used for setting up a listener + async subscribe(startSequence, doHandle) { + this.publish({ cmd: 'subscribe', startSequence }); + this.lastSequence = startSequence; + this.listener = { doHandle }; + } + // Used to tear down a listener + async unsubscribe() { + await this.publish({ cmd: 'unsubscribe' }); + this.listener = {}; + } + // Delete messages up to a certain sequence number + async deleteMessages(sequence) { + await this.publish({ cmd: 'deleteMessages', sequence }); + } + // Get the current state object + async getState() { + return await this.publish({ cmd: 'getState' }); + } + // Put the current state object + async putState(state) { + return await this.publish({ cmd: 'putState', state }); + } + // Head object from R2 + async headObject(key = '') { + try { + const res = await axios_1.default.head(`${this.r2Url}/${key}?apiKey=${this.apiKey}`); + return res.data; + } + catch (err) { + return err; + } + } + // Get object from R2 + async getObject(key) { + try { + const res = await axios_1.default.get(`${this.r2Url}/${key}?apiKey=${this.apiKey}`, { responseType: 'arraybuffer' }); + return res.data; + } + catch (err) { + return err; + } + } + // Put object to R2 + async putObject(key, file) { + try { + const res = await axios_1.default.post(`${this.r2Url}/${key}?apiKey=${this.apiKey}`, fs.createReadStream(file), { + maxBodyLength: Infinity, maxContentLength: Infinity + }); + return res.data; + } + catch (err) { + return err; + } + } + // Delete object from R2 + async deleteObject(key) { + try { + const res = await axios_1.default.delete(`${this.r2Url}/${key}?apiKey=${this.apiKey}`); + return res.data; + } + catch (err) { + return err; + } + } +} +exports.default = DurableStreamClient; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c764807 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,489 @@ +{ + "name": "durable-stream-client", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "durable-stream-client", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "axios": "^0.27.2", + "websocket": "^1.0.34" + }, + "devDependencies": { + "@types/node": "^18.7.14", + "@types/websocket": "^1.0.5", + "typescript": "^4.8.2" + } + }, + "node_modules/@types/node": { + "version": "18.7.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.14.tgz", + "integrity": "sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==", + "dev": true + }, + "node_modules/@types/websocket": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.5.tgz", + "integrity": "sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/bufferutil": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz", + "integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, + "node_modules/follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, + "node_modules/node-gyp-build": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", + "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz", + "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/utf-8-validate": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz", + "integrity": "sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/websocket": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", + "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", + "dependencies": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "engines": { + "node": ">=0.10.32" + } + } + }, + "dependencies": { + "@types/node": { + "version": "18.7.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.14.tgz", + "integrity": "sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==", + "dev": true + }, + "@types/websocket": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.5.tgz", + "integrity": "sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "bufferutil": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz", + "integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==", + "requires": { + "node-gyp-build": "^4.3.0" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "requires": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "requires": { + "type": "^2.7.2" + }, + "dependencies": { + "type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + } + } + }, + "follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, + "node-gyp-build": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", + "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==" + }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz", + "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==", + "dev": true + }, + "utf-8-validate": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz", + "integrity": "sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q==", + "requires": { + "node-gyp-build": "^4.3.0" + } + }, + "websocket": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", + "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", + "requires": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + } + }, + "yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fccfa18 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "durable-stream-client", + "version": "1.0.0", + "description": "Client application for durable-stream", + "main": "./lib/main.js", + "module": "./lib/main.js", + "types": "./src/index.d.ts", + "scripts": { + "build": "tsc --build", + "watch": "tsc -w" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/voxoco/durable-stream-client.git" + }, + "author": "Joe Mordica", + "license": "MIT", + "bugs": { + "url": "https://github.com/voxoco/durable-stream-client/issues" + }, + "homepage": "https://github.com/voxoco/durable-stream-client#readme", + "dependencies": { + "axios": "^0.27.2", + "websocket": "^1.0.34" + }, + "devDependencies": { + "@types/node": "^18.7.14", + "@types/websocket": "^1.0.5", + "typescript": "^4.8.2" + } +} diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..31721c5 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,16 @@ +declare module 'durable-stream-client' { + export default class DurableStreamClient { + constructor(obj: {host: string, secure: boolean, apiKey: string, subject: string}); + init(): Promise; + publish(msg: any): Promise; + info(): Promise; + subscribe(startSequence: number, doHandle: any): Promise; + unsubscribe(): Promise; + deleteMessages(sequence: number): Promise; + getState(): Promise; + putState(state: Object): Promise; + headObject(key: string): Promise; + getObject(key: string): Promise; + putObject(key: string, file: string): Promise; + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..a5696cf --- /dev/null +++ b/src/main.ts @@ -0,0 +1,216 @@ +import axios from 'axios'; +import * as fs from 'fs'; + +export default class DurableStreamClient { + ws: any; + waitingOperations: {[key: string]: any}; + cMsgId: number; + clientId: number; + listener: {[key: string]: any}; + wsUrl: string; + isConnected: boolean; + reconnects: number; + r2Url: string; + apiKey: string; + lastSequence: number; + host: string; + + constructor(obj: {host: string, secure: boolean, apiKey: string, subject: string}) { + + // Sanity check the api key + if (!obj.apiKey) throw new Error('No apiKey exists'); + + this.host = obj.host; + + // Base url for R2 + this.apiKey = obj.apiKey; + this.r2Url = `${obj.secure ? 'https' : 'http'}://${obj.host}/r2`; + + // Set the ws connection url + this.wsUrl = `${obj.secure ? 'wss' : 'ws'}://${obj.host}/stream/${obj.subject}?apiKey=${obj.apiKey}`; + this.ws = new WebSocket(this.wsUrl); + + // Storing messages we are waiting for a response to + this.waitingOperations = {}; + + // Message id counter and unique client id + const rand = Math.random(); + this.cMsgId = rand; + this.clientId = rand; + + // Listener for messages being broadcasted from the server + this.listener = {}; + + // Some state variables + this.isConnected = false; + this.reconnects = -1; + this.lastSequence = 0; + } + + async init() { + while (true) { + this.isConnected = false; + // Break out if we are connected + if (this.ws.readyState === this.ws.OPEN) { + await new Promise(resolve => setTimeout(resolve, 1000)); + this.reconnects++; + this.isConnected = true; + + console.log(`Connected to ${this.wsUrl} with ${this.reconnects} reconnects`); + + // Re-establish listener if we have one + if (this.listener.doHandle) { + console.log(`Re-establishing listener at sequence ${this.lastSequence}`); + this.subscribe(this.lastSequence, this.listener.doHandle); + } + break; + } + + // Reconnect if we are disconnected + if (this.ws.readyState === this.ws.CLOSED || this.ws.readyState === this.ws.CLOSING) { + console.log(`Attempting websocket connection to: ${this.host}`); + this.ws = new WebSocket(this.wsUrl); + } + + // Run this function until we are connected + if (this.ws.readyState === this.ws.CONNECTING) { + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + + // Setup listeners + await this.setupListeners(); + } + + // Setup ws client listeners + async setupListeners() { + + const closeHandler = async () => { + console.log('Session closed'); + await this.init(); + } + + this.ws.addEventListener("close", closeHandler); + this.ws.addEventListener("error", closeHandler); + + // Listen for messages + this.ws.addEventListener('message', async (msg: any) => { + const json = JSON.parse(msg.data); + + // Message from the server we should be subscribed to + if (json.pub) { + delete json.pub; + this.listener.doHandle(json, async () => { + this.lastSequence = json.sequence; + this.ws.send(JSON.stringify(json)); + }) + return; + } + + // This must be a message from us to the server we are waiting for a response to + if (json.cMsgId) { + this.waitingOperations[json.cMsgId].resolveMe(json); + delete this.waitingOperations[json.cMsgId]; + return; + } + + // Message from the server we need to respond to + if (json.sMsgId) { + this.ws.send(JSON.stringify(json)); + return; + } + }) + } + + // Send a message to the websocket waiting for a response + async publish(msg: any) { + // Check can send + if (!await this.canSend()) return {error: 'Could not send message'}; + + return new Promise((resolve) => { + ++this.cMsgId; + this.waitingOperations[this.cMsgId] = { resolveMe: resolve, data: msg, cMsgId: this.cMsgId, clientId: this.clientId }; + this.ws.send(JSON.stringify({data: msg, cMsgId: this.cMsgId, clientId: this.clientId})); + }) + } + + // Check if we are able to send a message + async canSend() { + let tries = 0; + while (!this.isConnected) { + await new Promise(resolve => setTimeout(resolve, 500)); + tries++; + console.log(`Waiting for connection... Trying ${tries} out of 20 (2 seconds)`); + if (tries > 20) break; + } + // If we have used up all our tries, return false + return tries < 20; + } + + // Get info about the stream + async info() { + return await this.publish({cmd: 'getStreamInfo'}); + } + + // Used for setting up a listener + async subscribe(startSequence: number, doHandle: any) { + this.publish({cmd: 'subscribe', startSequence}); + this.lastSequence = startSequence; + this.listener = {doHandle}; + } + + // Used to tear down a listener + async unsubscribe() { + await this.publish({cmd: 'unsubscribe'}); + this.listener = {}; + } + + // Delete messages up to a certain sequence number + async deleteMessages(sequence: number) { + await this.publish({cmd: 'deleteMessages', sequence}); + } + + // Get the current state object + async getState() { + return await this.publish({cmd: 'getState'}); + } + + // Put the current state object + async putState(state: Object) { + return await this.publish({cmd: 'putState', state}); + } + + // Head object from R2 + async headObject(key: string = '') { + try { + const res = await axios.head(`${this.r2Url}/${key}?apiKey=${this.apiKey}`); + return res.data; + } catch (err) {return err} + } + + // Get object from R2 + async getObject(key: string) { + try { + const res = await axios.get(`${this.r2Url}/${key}?apiKey=${this.apiKey}`, { responseType: 'arraybuffer' }); + return res.data; + } catch (err) {return err} + } + + // Put object to R2 + async putObject(key: string, file: string) { + try { + const res = await axios.post(`${this.r2Url}/${key}?apiKey=${this.apiKey}`, fs.createReadStream(file), { + maxBodyLength: Infinity, maxContentLength: Infinity + }); + return res.data; + } catch (err) {return err} + } + + // Delete object from R2 + async deleteObject(key: string) { + try { + const res = await axios.delete(`${this.r2Url}/${key}?apiKey=${this.apiKey}`); + return res.data; + } catch (err) {return err} + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6b717d3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,106 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [ "es6", "es2021", "esnext" ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": false, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./lib", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": [ + "./src" + ] +}