├── .gitignore ├── README.md ├── cli ├── package.json ├── tsconfig.json └── watch.ts ├── client-raw ├── README.md ├── client.ts ├── package.json ├── tsconfig.json └── types.ts ├── client ├── README.md ├── index.ts ├── package.json ├── tsconfig.json └── yarn-error.log ├── examples ├── 1 │ ├── README.md │ ├── client.js │ ├── package.json │ ├── server.js │ └── web │ │ ├── client.js │ │ └── index.html ├── 2 │ ├── client.js │ ├── package.json │ └── server.js ├── 3 │ ├── README.md │ ├── client.ts │ ├── package.json │ ├── server.ts │ ├── shared.ts │ └── tsconfig.json └── 2b │ ├── client.js │ ├── package.json │ └── server.js ├── package.json └── server ├── README.md ├── package.json ├── src ├── StringLike.ts ├── index.ts ├── stream.ts └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .*.swp 4 | bundle.js 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | This is a simple implementation of the braid protocol. 4 | 5 | It implements simple server and client implementations of the protocol, for use in nodejs and the browser. 6 | 7 | # Getting started 8 | 9 | https://www.npmjs.com/package/@braid-protocol/server 10 | 11 | https://www.npmjs.com/package/@braid-protocol/client 12 | 13 | https://www.npmjs.com/package/@braid-protocol/cli 14 | 15 | https://www.npmjs.com/package/@braid-protocol/client-raw 16 | 17 | ## From source 18 | 19 | This repository uses a tiny monorepo style. After git cloning, run `yarn` from the root directory of this repository to set everything up. 20 | 21 | # Code Examples 22 | 23 | I want to explore a few use cases. From simplest to most complicated: 24 | 25 | 1. Simple series of values which changes over time. (Eg CPU temperature, clock, price of bitcoin) 26 | - Each update just sends the new value 27 | - No version information 28 | - Either read-only or updates just set a new value 29 | 2. Document which sends changes using patches 30 | - When a client connects the first value will contain a document snapshot 31 | - Subsequent messages contain patches using some `patch-type` 32 | - The client has to opt in to the patch type (using the `accept-patch` header) 33 | 3. Versioned document 34 | - As above, but the document has a persistent version identifier 35 | - Each update from the server (the first, and subsequent updates) tell the client the new version identifier 36 | - The client can specify a known version when it connects: 37 | - If the known version is the latest version, the server does not need to send data in its initial response 38 | - If the known version is old the server decides whether to send the missing intervening patches or just send the client a fresh snapshot 39 | 40 | For each case I want: 41 | 42 | - A server (express) 43 | - Some simple client code 44 | 45 | I also want a simple web UI which can connect to any supported URL and show the value updating over time. 46 | 47 | 48 | # License 49 | 50 | > ISC license 51 | 52 | Copyright © 2020-2021 53 | 54 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 55 | 56 | THE SOFTWARE IS PROVIDED “AS IS” AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 57 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@braid-protocol/cli", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@braid-protocol/client": "^1", 9 | "@types/node": "^14.14.35", 10 | "chalk": "^4.1.0", 11 | "ts-node": "^9.1.1" 12 | }, 13 | "scripts": { 14 | "prepare": "tsc" 15 | }, 16 | "devDependencies": { 17 | "typescript": "^4.1.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cli/watch.ts: -------------------------------------------------------------------------------- 1 | import {subscribe} from '@braid-protocol/client' 2 | import {default as chalk} from 'chalk' 3 | import {inspect} from 'util' 4 | 5 | 6 | 7 | // const EventSource = require('eventsource') 8 | // const {inspect} = require('util') 9 | // const chalk = require('chalk') 10 | 11 | const verbose = true // TODO: Set me with command line flags 12 | const url = process.argv[2] || 'http://localhost:2001/time' 13 | 14 | ;(async () => { 15 | const {initialValue, initialVersion, streamHeaders, updates} = await subscribe(url) 16 | 17 | console.log('initial value', initialValue) 18 | if (initialVersion != null) console.log('initial version', initialVersion) 19 | 20 | for await (const {value, update, version} of updates) { 21 | console.clear() 22 | // console.log('value', value) 23 | // if (version != null) console.log('version', version) 24 | // console.log('update', update) 25 | 26 | const updateHeaders = update.headers 27 | for (const k in updateHeaders) { 28 | if (k !== 'version') { 29 | console.log(`${chalk.yellow(k)}: ${updateHeaders[k]}`) 30 | } 31 | } 32 | console.log() 33 | 34 | if (version != null) { 35 | console.log(`${chalk.cyan('version')}: ${chalk.cyan(version)}`) 36 | } else { 37 | console.log(`${chalk.cyan('version')}: ${chalk.red('unset')}`) 38 | } 39 | 40 | console.log( 41 | `${chalk.cyan('value')}:`, 42 | inspect(value, {compact: false, depth: null, colors: process.stdout.isTTY}) 43 | ) 44 | 45 | if (verbose) { 46 | console.log() 47 | 48 | if (update.type !== 'snapshot') { 49 | console.log( 50 | `${chalk.cyan('last change')}:`, 51 | inspect(update.patches, {compact: false, depth: null, colors: process.stdout.isTTY}) 52 | ) 53 | console.log(`${chalk.cyan('at')}:`, new Date().toLocaleTimeString()) 54 | } 55 | 56 | } 57 | } 58 | })() 59 | 60 | // let verbose = true // TODO: Set me with command line flags 61 | // let streamHeaders = {} 62 | // let patchType // 'full-snapshot' / 'update-keys' / ...? 63 | 64 | // let isFirst = true 65 | // let value 66 | 67 | // const merge = (patchType, patch) => { 68 | // // console.log('merge', patchType) 69 | // switch (patchType) { 70 | // case 'full-snapshot': return patch 71 | // case 'update-keys': { 72 | // // This just merges the two objects together. 73 | // return {...value, ...patch} 74 | // } 75 | // default: { 76 | // console.error('Unknown patch type', patchType) 77 | // return patch 78 | // } 79 | // } 80 | // } 81 | 82 | // es.onmessage = e => { 83 | // const message = JSON.parse(e.data) 84 | // let {headers, version, data} = message 85 | 86 | // console.clear() 87 | 88 | // if (isFirst) { 89 | // // TODO: Lowercase all values here. 90 | // if (headers != null) streamHeaders = {...streamHeaders, ...headers} 91 | // patchType = streamHeaders['x-patch-type'] || 'full-snapshot' 92 | // value = data 93 | // isFirst = false 94 | // } else { 95 | // value = merge(patchType, data) 96 | // } 97 | 98 | // if (streamHeaders) { 99 | // for (const k in streamHeaders) { 100 | // console.log(`${chalk.yellow(k)}: ${streamHeaders[k]}`) 101 | // } 102 | // console.log() 103 | // } 104 | // if (version != null) { 105 | // console.log(`${chalk.cyan('version')}: ${chalk.cyan(version)}`) 106 | // } else { 107 | // console.log(`${chalk.cyan('version')}: ${chalk.red('unset')}`) 108 | // } 109 | 110 | // console.log( 111 | // `${chalk.cyan('value')}:`, 112 | // inspect(value, {compact: false, depth: null, colors: process.stdout.isTTY}) 113 | // ) 114 | 115 | // if (verbose) { 116 | // console.log() 117 | 118 | // if (patchType !== 'full-snapshot') { 119 | // console.log( 120 | // `${chalk.cyan('last change')}:`, 121 | // inspect(data, {compact: false, depth: null, colors: process.stdout.isTTY}) 122 | // ) 123 | // console.log(`${chalk.cyan('at')}:`, new Date().toLocaleTimeString()) 124 | // } 125 | 126 | // } 127 | // } -------------------------------------------------------------------------------- /client-raw/README.md: -------------------------------------------------------------------------------- 1 | # Braid protocol raw client 2 | 3 | This library is a simple reference implementation of the client side of the [braid protocol](https://github.com/braid-org/braid-spec/). 4 | 5 | This code parses and returns updates sent over a braid subscription. Each update in a braid subscription is either: 6 | 7 | - A snapshot (a replacement of the entire document contents), or 8 | - A set of patches. Each patch has a patchType specifying the type of the patch, headers and content. 9 | 10 | Unlike the high level braid library, this implementation does not try to interpret or apply any received patches. Most applications will probably want to use the higher level client library - which returns a stream of document values. 11 | 12 | This library (currently) offers no assistance in making it easy to modify server data. 13 | 14 | ## Getting started 15 | 16 | ``` 17 | npm install --save @braid-protocol/client-raw 18 | ``` 19 | 20 | Then: 21 | 22 | ```javascript 23 | const { subscribe } = require('@braid-protocol/client-raw') 24 | 25 | ;(async () => { 26 | const { updates } = await subscribe('http://localhost:2001/time') 27 | for await (const { type, headers, value, patches } of updates) { 28 | if (type === 'snapshot') { 29 | console.log('new value:', value) 30 | } else { 31 | console.log('got patches:', patches) 32 | } 33 | } 34 | })() 35 | ``` 36 | 37 | This code will work from either a web browser or from nodejs. 38 | 39 | 40 | ## API 41 | 42 | This module exposes 2 methods: *subscribeRaw* and *subscribe*. 43 | 44 | - *subscribeRaw* returns updates using raw Uint8Arrays, and makes no attempt to parse any of the braid-specific headers 45 | - *subscribe* will parse out version, content-type and patch-type headers from updates. It will also attempt to convert the contents of patches and updates into JSON objects or strings, based on the specified content-type of responses. 46 | 47 | 48 | #### subscribeRaw 49 | 50 | > **subscribeRaw(url: string, opts?) => Promise<{streamHeaders, updates}>** 51 | 52 | subscribeRaw initiates an HTTP request to the specified URL and attempts to start a subscription. 53 | 54 | The options field is optional. If present, it can contain the following fields: 55 | 56 | - **reqHeaders**: An object containing additional headers to be sent as part of the HTTP request 57 | - *(More will be added later - we also need additional options for the fetch request, like `allowCredentials`). Please file issues / send PRs if we're missing something important here.* 58 | 59 | subscribeRaw returns a promise containing: 60 | 61 | - **streamHeaders**: An object containing HTTP-level headers returned by the server. Eg `{content-type: 'application/json'}`. 62 | - **updates**: An async iterator which yields updates as they are sent from the server to the client. 63 | 64 | You can consume the iterator using a for-await loop: 65 | 66 | ```javascript 67 | const { updates } = await subscribeRaw('http://localhost:2001/time') 68 | 69 | for await (const { type, headers, value, patches } of updates) { 70 | console.log('got update of type', type, 'with headers', headers) 71 | } 72 | ``` 73 | 74 | Each entry in the updates iterator contains: 75 | 76 | - **type**: Either `'snapshot'` or `'patch'`, specifying if the `value` or `patches` field is included in the response, respectively. 77 | - **headers**: Update level headers (duh) 78 | - **value**: Snapshot updates contain a Uint8Array with the new document contents 79 | - **patches**: A list of patches returned by the server. Each patch contains headers and a body field, with the body again being a Uint8Array. 80 | 81 | 82 | #### subscribe 83 | 84 | > **subscribe(url: string, opts?) => Promise<{streamHeaders, updates}>** 85 | 86 | This method is a wrapper around `subscribeRaw`, doing some best-guess parsing of the returned content. 87 | 88 | > TODO: Document me! 89 | -------------------------------------------------------------------------------- /client-raw/client.ts: -------------------------------------------------------------------------------- 1 | import { RawPatch, RawUpdateData, Patch, UpdateData } from './types' 2 | import 'isomorphic-fetch' 3 | import { Readable } from 'stream' 4 | 5 | export * from './types' 6 | 7 | const splitOnce = ( 8 | s: string, 9 | sep: string | RegExp 10 | ): [string, string] | null => { 11 | const pos = s.search(sep) 12 | if (pos < 0) return null 13 | else { 14 | const remainder = s.slice(pos) 15 | // Figure out the length of the separator using the regular expression 16 | 17 | // TODO: Why am I calling match twice, here and above? Fix to only do that 18 | // once. 19 | const sepLen = 20 | typeof sep === 'string' ? sep.length : remainder.match(sep)![0].length 21 | return [s.slice(0, pos), remainder.slice(sepLen)] 22 | } 23 | } 24 | 25 | const concatBuffers = (a: Uint8Array, b: Uint8Array) => { 26 | const result = new Uint8Array(a.length + b.length) 27 | result.set(a, 0) 28 | result.set(b, a.length) 29 | return result 30 | } 31 | 32 | const asciiDecoder = new TextDecoder('ascii') 33 | const headerSepRegex = /\r?\n\r?\n/ 34 | 35 | /** 36 | * Search the buffer for the \r\n\r\n between header and data. 37 | * 38 | * This method returns null if the header has not been terminated, or 39 | * the header string + byte offset of the body. 40 | */ 41 | const searchHeaderGap = (buf: Uint8Array): null | [string, number] => { 42 | // We have some confidence the header will be pure ASCII - but even if 43 | // not, I think the ascii decoder should always preserve byte length. 44 | // (I hope.) 45 | 46 | // There's no good methods for doing substring search in an 47 | // arraybuffer in javascript, so we'll (somewhat inefficiently) do 48 | // extra decoding operations. This is probably fine - though will 49 | // become inefficient if the chunk size is particularly large. There's 50 | // some libraries on npm which would improve perf here. 51 | const s = asciiDecoder.decode(buf) 52 | const match = s.match(headerSepRegex) 53 | 54 | if (match == null) return null 55 | else { 56 | return [s.slice(0, match.index), match.index! + match[0].length] 57 | } 58 | } 59 | 60 | 61 | // Reused. (Why is this a stateful API?) 62 | const decoder = new TextDecoder() 63 | 64 | async function* readHTTPChunks(res: Response): AsyncGenerator { 65 | // Medium-sized state machine. We swap back and forth from reading the headers <-> 66 | // reading data. Every chunk must contain a content-length field. If we encounter 67 | // a Patches header, we count out the patches (each with headers & content) and return 68 | // them inside a version. 69 | 70 | // An alternate implementation here could make another generator and 71 | // lean on the compiler-generated generator's state machine. 72 | 73 | const enum State { 74 | UpdateHeaders, 75 | UpdateContent, 76 | PatchHeaders, 77 | PatchContent, 78 | } 79 | let state = State.UpdateHeaders 80 | let buffer = new Uint8Array() 81 | let versionHeaders: Record | null = null 82 | let patchHeaders: Record | null = null 83 | let patches: Array = [] 84 | let patchesCount: number = 0 85 | 86 | // Trim leading newlines (if any) in buffer 87 | const trimNewlines = () => { 88 | const enum Char { 89 | LineSep = 10, 90 | RecordSep = 13, 91 | } 92 | let i = 0 93 | while (i < buffer.length && (buffer[i] === Char.LineSep || buffer[i] === Char.RecordSep)) { 94 | i++ 95 | } 96 | if (i > 0) buffer = buffer.slice(i) 97 | } 98 | 99 | function getNextHeaders(): Record | null { 100 | // Ok, so there's a problem here: We need to search for the 101 | // double-newline seperator between header and data. The header 102 | // should be pure ASCII, but the data section can (and will) contain 103 | // utf8 characters. These characters may be split between message 104 | // chunks, too! And the named content-length is specified in bytes. 105 | 106 | // Extra newlines between messages are used for heartbeats. 107 | trimNewlines() 108 | 109 | const headerData = searchHeaderGap(buffer) 110 | if (headerData == null) return null 111 | else { 112 | const [headerStr, dataOffset] = headerData 113 | 114 | const headers = Object.fromEntries( 115 | headerStr.split(/\r?\n/).map((entry) => { 116 | const kv = splitOnce(entry, ': ') 117 | if (kv == null) throw Error(`invalid HTTP header: ${entry}`) 118 | else return [kv[0].toLowerCase(), kv[1]] 119 | }) 120 | ) 121 | 122 | buffer = buffer.slice(dataOffset) 123 | return headers 124 | } 125 | } 126 | 127 | function* append(s: Uint8Array): Generator { 128 | // This is pretty inefficient, but it's probably fine. 129 | buffer = concatBuffers(buffer, s) 130 | 131 | // This is the beating heart of the client. The braid protocol is an 132 | // HTTP stream with HTTP inside it. To parse braid messages we need 133 | // an HTTP parser - so this is a simple low performance 134 | // implementation for parsing the HTTP messages we get on the 135 | // stream. 136 | // 137 | // At the highest level the HTTP stream contains *updates*. Each 138 | // update contains either a replacement document snapshot, or a set 139 | // of one or more patches. 140 | // 141 | // Each time append() is called, we have some more bytes to process. 142 | // The extra data may have any framing - we might get an entire 143 | // update, or updates and patches might be chunked in any imaginable 144 | // way by intermediate proxies. In the loop below we'll read as much 145 | // as we can. Any received updates are chunked out and yielded to 146 | // the caller. 147 | // 148 | // This method has gotten more complex. It might be worth rewriting 149 | // it at some point as an async iterator where chunks are awaited. 150 | // This would make the manually written state machine simpler to 151 | // read, in exchange for more magic and probably a slight drop in 152 | // performance. 153 | 154 | loop: while (true) { 155 | // We're in 1 of 4 states 156 | switch (state) { 157 | case State.UpdateHeaders: { 158 | const nextHeaders = getNextHeaders() 159 | if (nextHeaders == null) break loop // need more bytes 160 | 161 | versionHeaders = nextHeaders 162 | if (versionHeaders['patches']) { 163 | patchesCount = parseInt(versionHeaders['patches']) 164 | state = State.PatchHeaders 165 | } else { 166 | state = State.UpdateContent 167 | } 168 | break 169 | } 170 | 171 | case State.UpdateContent: { 172 | if (versionHeaders == null) throw Error('invalid state') 173 | 174 | const contentLength = versionHeaders['content-length'] 175 | if (contentLength == null) throw Error('Content-Length or Patches required') 176 | 177 | const contentLengthNum = parseInt(contentLength) 178 | if (isNaN(contentLengthNum) || contentLengthNum < 0) { 179 | throw Error('invalid Content-Length') 180 | } 181 | 182 | if (buffer.length < contentLengthNum) break loop // need more bytes 183 | 184 | const value = buffer.slice(0, contentLengthNum) 185 | yield { 186 | type: 'snapshot', 187 | headers: versionHeaders, 188 | value 189 | } 190 | buffer = buffer.slice(contentLengthNum) 191 | versionHeaders = null 192 | 193 | state = State.UpdateHeaders 194 | break 195 | } 196 | 197 | case State.PatchHeaders: { 198 | if (versionHeaders == null) throw Error('invalid state') 199 | 200 | patchHeaders = getNextHeaders() 201 | if (patchHeaders == null) break loop // need more bytes 202 | 203 | // Move straight on to reading the patch contents. 204 | state = State.PatchContent 205 | } // continued 206 | 207 | case State.PatchContent: { 208 | if (versionHeaders == null) throw Error('invalid state') 209 | if (patchHeaders == null) throw Error('invalid state') 210 | 211 | const contentLength = patchHeaders['content-length'] 212 | if (contentLength == null) { 213 | throw Error('Patch is missing Content-Length header') 214 | } 215 | 216 | const contentLengthNum = parseInt(contentLength) 217 | if (isNaN(contentLengthNum) || contentLengthNum < 0) { 218 | throw Error('invalid Content-Length') 219 | } 220 | 221 | if (buffer.length < contentLengthNum) break loop // more bytes plz 222 | 223 | const body = buffer.slice(0, contentLengthNum) 224 | 225 | // We don't yield yet, because we need all the patches for this 226 | // version together. Push onto array and send later. 227 | patches.push({ headers: patchHeaders, body }) 228 | buffer = buffer.slice(contentLengthNum) 229 | patchHeaders = null 230 | 231 | // One fewer patches in this version to process 232 | patchesCount-- 233 | 234 | if (patchesCount == 0) { 235 | yield { 236 | type: 'patch', 237 | headers: versionHeaders, 238 | patches 239 | } 240 | state = State.UpdateHeaders 241 | patches = [] 242 | versionHeaders = null 243 | patchHeaders = null 244 | } else { 245 | // Go back to processing next patch (or done) 246 | state = State.PatchHeaders 247 | } 248 | 249 | break 250 | } 251 | } 252 | } 253 | } 254 | 255 | // Apparently node-fetch doesn't implement the WhatWG's stream protocol for 256 | // some reason. Instead it shows up as a nodejs stream. 257 | if (res.body && (res.body as any)[Symbol.asyncIterator] != null) { 258 | // We're in nodejs land, and the body is a nodejs stream object. 259 | const body = (res.body as any) as Readable 260 | 261 | // There's a bug where none of these events fire when the underlying TCP 262 | // connection disappears. Its fixed in node-fetch@next. 263 | 264 | // body.on('error', err => console.error(err)) 265 | // body.on('end', () => console.log('end')) 266 | // body.on('close', () => console.log('close')) 267 | 268 | for await (const item of body) { 269 | yield* append(item) 270 | } 271 | } else { 272 | // We're in browser land and we can get a ReadableStream 273 | const reader = res.body!.getReader() 274 | // reader.closed.then(() => console.log('closed'), err => console.error('err', err)) 275 | 276 | try { 277 | while (true) { 278 | const { value, done } = await reader.read() 279 | if (done) break 280 | yield* append(value!) 281 | } 282 | } catch (e) { 283 | console.warn('Connection died', e) 284 | } 285 | } 286 | } 287 | 288 | export interface RawSubscribeOpts { 289 | reqHeaders?: Record 290 | } 291 | 292 | export async function subscribeRaw(url: string, opts: RawSubscribeOpts = {}) { 293 | const res = await fetch(url, { 294 | // url, 295 | headers: { 296 | subscribe: 'keep-alive', 297 | ...opts.reqHeaders, 298 | }, 299 | }) 300 | 301 | return { 302 | streamHeaders: Object.fromEntries(res.headers), 303 | updates: readHTTPChunks(res), 304 | } 305 | } 306 | 307 | // ***** TODO: API boundary here. 308 | 309 | 310 | const defaultParseDoc = (contentType: string | null, content: Uint8Array): any => ( 311 | // This is vastly incomplete and a compatibility nightmare. 312 | contentType == null ? content 313 | : contentType.startsWith('text/') ? decoder.decode(content) 314 | : contentType.startsWith('application/json') ? JSON.parse(decoder.decode(content)) 315 | : content 316 | ) 317 | 318 | const defaultParsePatch = (patchType: string | null, headers: Record, data: Uint8Array): any => ( 319 | // This is woefully wrong. Amongst other things, this needs to handle 320 | // braid patches. 321 | JSON.parse(decoder.decode(data)) 322 | ) 323 | 324 | interface SubscribeOpts extends RawSubscribeOpts { 325 | parseDoc?: (contentType: string | null, content: Uint8Array) => Doc 326 | parsePatch?: (patchType: string | null, headers: Record, content: Uint8Array) => Patch 327 | } 328 | 329 | /** 330 | * This is a variant of subscribeRaw which (roughly) parses converts 331 | * patch / snapshot based on the content-type header in the parent 332 | * response. This may or may not actually be correct according to the 333 | * protocol... 334 | */ 335 | export async function subscribe(url: string, opts: SubscribeOpts = {}) { 336 | const { streamHeaders, updates: updateStream } = await subscribeRaw(url, opts) 337 | const contentType = streamHeaders['content-type'] as string | undefined ?? null 338 | // Assuming https://github.com/braid-org/braid-spec/issues/106 is accepted 339 | const patchType = streamHeaders['patch-type'] as string | undefined 340 | const currentVersions = (streamHeaders['current-versions'] as string | undefined) ?? null 341 | const parsePatch = opts.parsePatch ?? defaultParsePatch 342 | const parseDoc = opts.parseDoc ?? defaultParseDoc 343 | 344 | async function* consumeVersions(): AsyncGenerator> { 345 | for await (const update of updateStream) { 346 | const {headers} = update 347 | if (update.type === 'snapshot') { 348 | const value = parseDoc(contentType, update.value) 349 | yield { 350 | type: 'snapshot', 351 | headers, 352 | version: headers['version'] ?? null, 353 | contentType, 354 | value, 355 | } 356 | } else { 357 | const patches = update.patches.map(patch => { 358 | // patch-type header is defined in https://github.com/braid-org/braid-spec/issues/97 . 359 | const range = patch.headers['content-range'] as string | undefined 360 | const localPatchType = patch.headers['content-type'] 361 | ?? patch.headers['patch-type'] 362 | ?? (range != null ? 'braid' : null) 363 | ?? patchType 364 | ?? 'unknown' 365 | 366 | // console.log('got patch content', patch.data, `'${decoder.decode(patch.data)}'`) 367 | return { 368 | headers: patch.headers, 369 | patchType: localPatchType, 370 | range, 371 | body: parsePatch(contentType, patch.headers, patch.body), 372 | } as Patch

373 | }) 374 | yield { 375 | type: 'patch', 376 | headers, 377 | version: headers['version'] ?? null, 378 | patches, 379 | } 380 | } 381 | } 382 | } 383 | 384 | return { 385 | streamHeaders, 386 | currentVersions, 387 | contentType, 388 | updates: consumeVersions(), 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /client-raw/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@braid-protocol/client-raw", 3 | "version": "1.0.0", 4 | "main": "dist/client.js", 5 | "types": "dist/client.d.ts", 6 | "public": true, 7 | "license": "ISC", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/josephg/braid-protocol" 11 | }, 12 | "dependencies": { 13 | "isomorphic-fetch": "^3.0.0", 14 | "ministreamiterator": "^1.0.0", 15 | "unicount": "^1.1.0" 16 | }, 17 | "files": [ 18 | "dist/*" 19 | ], 20 | "scripts": { 21 | "prepare": "tsc" 22 | }, 23 | "devDependencies": { 24 | "typescript": "^4.1.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client-raw/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /client-raw/types.ts: -------------------------------------------------------------------------------- 1 | export type RawPatch = { 2 | headers: Record 3 | body: Uint8Array 4 | } 5 | 6 | export type RawSnapshotUpdate = { 7 | type: 'snapshot', 8 | headers: Record 9 | value: Uint8Array 10 | } 11 | 12 | export type RawPatchUpdate = { 13 | type: 'patch', 14 | headers: Record 15 | patches: Array 16 | } 17 | 18 | export type RawUpdateData = RawSnapshotUpdate | RawPatchUpdate 19 | 20 | /// *** 21 | 22 | 23 | // A string, with autocomplete. 24 | export type PatchType = 'braid' | 'ot-json1' | 'ot-text-unicode' | string 25 | 26 | export type Patch

= { 27 | headers: Record 28 | patchType: PatchType, 29 | range?: string, 30 | body: P // TODO: patch? Data? Value? ??? 31 | } 32 | 33 | export type SnapshotUpdate = { 34 | type: 'snapshot', 35 | headers: Record 36 | version: string | null, 37 | contentType: string | null, 38 | value: Doc // TODO: value or data? 39 | } 40 | 41 | export type PatchUpdate

= { 42 | type: 'patch', 43 | headers: Record 44 | version: string | null, 45 | patches: Array> 46 | } 47 | 48 | export type UpdateData = SnapshotUpdate | PatchUpdate

49 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This is the high level client API for braid. 2 | 3 | This is the API most people will probably want to use. Rather than 4 | exposing raw braid subscription messages (patches and snapshots), this 5 | API simply exposes an object which changes over time. 6 | 7 | This library has built-in support for the braid patch format (range 8 | requests), and it supports registering plugins for other custom patch 9 | types (eg ot-json1). -------------------------------------------------------------------------------- /client/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { RawSubscribeOpts, subscribe as subscribeInner, UpdateData } from '@braid-protocol/client-raw' 3 | 4 | export interface ClientOpts extends RawSubscribeOpts { 5 | parseDoc?: (contentType: string | null, content: Uint8Array) => Doc 6 | applyPatch?: (prevValue: Doc, patchType: string, patch: Uint8Array) => Doc 7 | 8 | /** 9 | * If the client knows the value of the document at some specified version, 10 | * set knownDoc and knownAtVersion. The server can elide intervening 11 | * operations and just bring the client up to date. 12 | */ 13 | knownDoc?: Doc 14 | knownAtVersion?: string 15 | 16 | /** 17 | * If knownAtVersion is behind the current server version, emit all 18 | * intermediate operations in the iterator. Do not wait until the client is 19 | * up-to-date before returning. 20 | * 21 | * Has no effect if knownAtVersion is unset. 22 | */ 23 | emitAllPatches?: boolean, 24 | 25 | /** Should the client automatically reconnect when disconnected? */ 26 | // reconnect?: boolean 27 | } 28 | 29 | // const merge = (prevValue: T, patchType: string, patch: any, opts: StateClientOptions): T => { 30 | // switch (patchType) { 31 | // case 'snapshot': return patch 32 | // case 'merge-keys': { 33 | // // This just merges the two objects together. 34 | // return {...prevValue, ...patch} 35 | // } 36 | // default: { 37 | // throw Error('Unknown patch type: ' + patchType) 38 | // // return patch 39 | // } 40 | // } 41 | // } 42 | 43 | 44 | /** 45 | * This is a high level API for subscribe. It supports: 46 | * 47 | * - Tracking the state of the document over time 48 | * - Reconnecting (well, it will) 49 | * - Waiting for the initial document verion before returning 50 | */ 51 | export async function subscribe( 52 | url: string, 53 | opts: ClientOpts = {} 54 | ) { 55 | // type PatchBundle = { 56 | // patchType: string, 57 | // headers: Record, 58 | // content: Uint8Array 59 | // } 60 | let value: any = opts.knownDoc 61 | let version: string | null = opts.knownAtVersion ?? null 62 | 63 | const reqHeaders: Record = {} 64 | if (opts.knownAtVersion != null) reqHeaders['version'] = opts.knownAtVersion 65 | 66 | const { streamHeaders, currentVersions, updates: updateStream } = await subscribeInner(url, { 67 | reqHeaders: opts.reqHeaders, 68 | parseDoc: opts.parseDoc, 69 | parsePatch: (patchType, headers, content) => content 70 | }) 71 | // const contentType = streamHeaders['content-type'] 72 | // const upToDateVersion: string | null = streamHeaders['current-versions'] ?? null 73 | // const patchType = streamHeaders['patch-type'] 74 | 75 | const apply = (update: UpdateData) => { 76 | if (update.version != null) version = update.version 77 | 78 | if (update.type === 'snapshot') { 79 | value = update.value 80 | } else { 81 | if (!opts.applyPatch) { 82 | throw Error('Cannot patch documents without an apply function') 83 | } 84 | 85 | for (const patch of update.patches) { 86 | value = opts.applyPatch(value, patch.patchType, patch.body) 87 | } 88 | } 89 | } 90 | 91 | // This method will wait until we have the first known-good value before 92 | // returning. There's three cases when this happens. Either: 93 | // 94 | // 1. The client already knows the value at the current version 95 | // (currentVersions === knownAtVersion). 96 | // 2. There is no known version. The first message from the server should have 97 | // a snapshot. Return that. 98 | // 3. We're behind. If opts.emitAllPatches then we emit immediately. Otherwise 99 | // wait until we're up to date before emitting anything. 100 | 101 | if (currentVersions == null) { 102 | // Case 2. 103 | // Consume the first patch no matter what. It will usually be a snapshot. 104 | const update = await updateStream.next() 105 | if (!update.done) apply(update.value) 106 | } else { 107 | // Cases 1 and 3 108 | if (!opts.emitAllPatches || version == null) { 109 | while (version !== currentVersions) { 110 | // Case 3. 111 | const update = await updateStream.next() 112 | if (update.done) break 113 | apply(update.value) 114 | } 115 | } // else case 1. 116 | } 117 | 118 | async function* consumePatches() { 119 | for await (const update of updateStream) { 120 | apply(update) 121 | yield { value, version, update } 122 | } 123 | } 124 | 125 | return { 126 | initialValue: value, 127 | initialVersion: version, 128 | streamHeaders, 129 | updates: consumePatches(), 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@braid-protocol/client", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "public": true, 7 | "license": "ISC", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/josephg/braid-protocol" 11 | }, 12 | "dependencies": { 13 | "@braid-protocol/client-raw": "1.0.0" 14 | }, 15 | "scripts": { 16 | "prepare": "tsc" 17 | }, 18 | "devDependencies": { 19 | "typescript": "^4.1.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /client/yarn-error.log: -------------------------------------------------------------------------------- 1 | Arguments: 2 | /home/seph/.fnm/node-versions/v15.3.0/installation/bin/node /tmp/fnm_multishell_70676_1616028563080/bin/yarn 3 | 4 | PATH: 5 | /tmp/fnm_multishell_70676_1616028563080/bin:/home/seph/.fnm:/tmp/fnm_multishell_70672_1616028563066/bin:/home/seph/.fnm:/home/seph/.cargo/bin:/home/seph/.bin:/home/seph/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin 6 | 7 | Yarn version: 8 | 1.22.10 9 | 10 | Node version: 11 | 15.3.0 12 | 13 | Platform: 14 | linux x64 15 | 16 | Trace: 17 | Error: https://registry.yarnpkg.com/@josephg%2fbraid-client-raw: Not found 18 | at Request.params.callback [as _callback] (/home/seph/.fnm/node-versions/v15.3.0/installation/lib/node_modules/yarn/lib/cli.js:66988:18) 19 | at Request.self.callback (/home/seph/.fnm/node-versions/v15.3.0/installation/lib/node_modules/yarn/lib/cli.js:140662:22) 20 | at Request.emit (node:events:376:20) 21 | at Request. (/home/seph/.fnm/node-versions/v15.3.0/installation/lib/node_modules/yarn/lib/cli.js:141634:10) 22 | at Request.emit (node:events:376:20) 23 | at IncomingMessage. (/home/seph/.fnm/node-versions/v15.3.0/installation/lib/node_modules/yarn/lib/cli.js:141556:12) 24 | at Object.onceWrapper (node:events:482:28) 25 | at IncomingMessage.emit (node:events:388:22) 26 | at endReadableNT (node:internal/streams/readable:1294:12) 27 | at processTicksAndRejections (node:internal/process/task_queues:80:21) 28 | 29 | npm manifest: 30 | { 31 | "name": "@braid-protocol/client", 32 | "version": "1.0.0", 33 | "main": "dist/client.js", 34 | "types": "dist/client.d.ts", 35 | "license": "MIT", 36 | "dependencies": { 37 | "@braid-protocol/client-raw": "*" 38 | }, 39 | "scripts": { 40 | "prepare": "tsc" 41 | }, 42 | "devDependencies": { 43 | "typescript": "^4.1.3" 44 | } 45 | } 46 | 47 | yarn manifest: 48 | No manifest 49 | 50 | Lockfile: 51 | No lockfile 52 | -------------------------------------------------------------------------------- /examples/1/README.md: -------------------------------------------------------------------------------- 1 | This first example shows how you can use this library to subscribe to a simple feed of values which change over time. This feed makes no use of versions or patches. 2 | 3 | To run this example, first run `yarn` from the root directory of this repository. Then in one terminal run: 4 | 5 | ``` 6 | $ node server.js 7 | ``` 8 | 9 | And in another terminal run: 10 | 11 | ``` 12 | $ node client.js 13 | ``` 14 | 15 | Or you can visit [localhost:2001](http://localhost:2001/) in your browser to see the web based demo. (Run `yarn prepare` to rebuild.) -------------------------------------------------------------------------------- /examples/1/client.js: -------------------------------------------------------------------------------- 1 | const { subscribe } = require('@braid-protocol/client-raw') 2 | 3 | ;(async () => { 4 | const { updates } = await subscribe('http://localhost:2001/time') 5 | for await (const { value } of updates) { 6 | console.log(value) 7 | } 8 | })() 9 | -------------------------------------------------------------------------------- /examples/1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example1", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "prepare": "browserify web/client.js -o web/bundle.js" 8 | }, 9 | "dependencies": { 10 | "@braid-protocol/client-raw": "*", 11 | "@braid-protocol/server": "*", 12 | "cors": "^2.8.5", 13 | "polka": "^0.5.2", 14 | "sirv": "^1.0.11" 15 | }, 16 | "devDependencies": { 17 | "browserify": "^17.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/1/server.js: -------------------------------------------------------------------------------- 1 | const polka = require('polka') 2 | const sirv = require('sirv') 3 | const cors = require('cors') 4 | const braid = require('@braid-protocol/server') 5 | 6 | const assets = sirv(__dirname + '/web') 7 | 8 | const getDate = () => new Date().toLocaleString() + '\n' 9 | 10 | polka() 11 | .options('/time', cors({methods: ['GET']})) 12 | .get('/time', cors(), (req, res) => { 13 | let timer 14 | 15 | if (req.headers.subscribe === 'keep-alive') { 16 | const stream = braid.stream(res, { 17 | initialValue: getDate(), 18 | contentType: 'text/plain', 19 | onclose() { 20 | clearInterval(timer) 21 | }, 22 | }) 23 | 24 | timer = setInterval(() => { 25 | stream.append({ value: getDate() }) 26 | }, 1000) 27 | } else { 28 | res.end(getDate()) 29 | } 30 | }) 31 | .use(assets) 32 | .listen(2001, (err) => { 33 | if (err) throw err 34 | console.log('listening on http://localhost:2001/time') 35 | console.log('Open http://localhost:2001/ in a browser for a simple demo') 36 | }) 37 | -------------------------------------------------------------------------------- /examples/1/web/client.js: -------------------------------------------------------------------------------- 1 | const { subscribe } = require('@braid-protocol/client-raw') 2 | 3 | const elem = document.getElementById('time') 4 | 5 | elem.innerText = 'loading' 6 | ;(async () => { 7 | const { updates } = await subscribe('http://localhost:2001/time') 8 | for await (const { value } of updates) { 9 | elem.innerText = value 10 | } 11 | elem.innerText = 'disconnected' 12 | })() 13 | -------------------------------------------------------------------------------- /examples/1/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | 6 | -------------------------------------------------------------------------------- /examples/2/client.js: -------------------------------------------------------------------------------- 1 | const { subscribe } = require('@braid-protocol/client-raw') 2 | const { type } = require('ot-text-unicode') 3 | 4 | ;(async () => { 5 | let value = undefined 6 | const { updates } = await subscribe('http://localhost:2002/doc') 7 | for await (const data of updates) { 8 | if (data.type === 'snapshot') { 9 | // Snapshot updates replace the current value 10 | value = data.value 11 | } else { 12 | // Or with patches we apply all the patches we're given in sequence 13 | for (const {body} of data.patches) { 14 | value = type.apply(value, body) 15 | } 16 | } 17 | console.log(value) 18 | } 19 | })() 20 | -------------------------------------------------------------------------------- /examples/2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example2", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@braid-protocol/client-raw": "*", 8 | "@braid-protocol/server": "*", 9 | "polka": "^0.5.2", 10 | "ot-fuzzer": "^1.3.0", 11 | "ot-text-unicode": "^4.0.0" 12 | }, 13 | "scripts": { 14 | "prepare": "node -e '`examples/2: nothing to do`'" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/2/server.js: -------------------------------------------------------------------------------- 1 | const polka = require('polka') 2 | const braid = require('@braid-protocol/server') 3 | 4 | const genOp = require('ot-text-unicode/test/genOp') 5 | let doc = 'hi there' 6 | 7 | // Set of clients to be updated. 8 | const clients = new Set() 9 | 10 | // Every second update the document by modifying it with a patch. 11 | setInterval(() => { 12 | const [op, result] = genOp(doc) 13 | // const [op, result] = [['👻'], '👻' + doc] 14 | doc = result 15 | 16 | const j = JSON.stringify(op) 17 | // console.log(j.length, j) 18 | 19 | for (const c of clients) { 20 | c.append({ 21 | patches: [JSON.stringify(op) + '\n'] 22 | }) 23 | } 24 | }, 1000) 25 | 26 | polka() 27 | .get('/doc', (req, res) => { 28 | const stream = braid.stream(res, { 29 | reqHeaders: req.headers, 30 | initialValue: doc + '\n', 31 | patchType: 'ot-text-unicode', 32 | contentType: 'text/plain', 33 | onclose() { 34 | if (stream) clients.delete(stream) 35 | }, 36 | }) 37 | if (stream) clients.add(stream) 38 | }) 39 | .listen(2002, (err) => { 40 | if (err) throw err 41 | console.log('listening on http://localhost:2002/doc') 42 | }) 43 | -------------------------------------------------------------------------------- /examples/2b/client.js: -------------------------------------------------------------------------------- 1 | const { subscribe } = require('@braid-protocol/client-raw') 2 | 3 | ;(async () => { 4 | const { updates } = await subscribe('http://localhost:2002/doc') 5 | const initialVersion = (await updates.next()).value 6 | console.log('initial value', initialVersion.value) 7 | for await (const version of updates) { 8 | console.log('patch', version.patches) 9 | } 10 | })() 11 | -------------------------------------------------------------------------------- /examples/2b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example2b", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@braid-protocol/client-raw": "*", 8 | "@braid-protocol/server": "*", 9 | "polka": "^0.5.2", 10 | "ot-fuzzer": "^1.3.0", 11 | "ot-text-unicode": "^4.0.0" 12 | }, 13 | "scripts": { 14 | "prepare": "node -e '`examples/2b: nothing to do`'" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/2b/server.js: -------------------------------------------------------------------------------- 1 | const polka = require('polka') 2 | const braid = require('@braid-protocol/server') 3 | 4 | let doc = [] 5 | 6 | // Set of clients to be updated. 7 | const clients = new Set() 8 | 9 | // Every second, update the document and append an item. 10 | setInterval(() => { 11 | const item = { item: doc.length } 12 | doc.push(item) 13 | for (const c of clients) { 14 | c.append({ 15 | patches: [{ 16 | range: '[-0:-0]', 17 | body: JSON.stringify(item) + '\n', 18 | }], 19 | }) 20 | } 21 | }, 1000) 22 | 23 | polka() 24 | .get('/doc', (req, res) => { 25 | const stream = braid.stream(res, { 26 | reqHeaders: req.headers, 27 | initialValue: JSON.stringify(doc) + '\n', 28 | contentType: 'application/json', 29 | onclose() { 30 | if (stream) clients.delete(stream) 31 | }, 32 | }) 33 | if (stream) clients.add(stream) 34 | }) 35 | .listen(2002, (err) => { 36 | if (err) throw err 37 | console.log('listening on http://localhost:2002/doc') 38 | }) 39 | -------------------------------------------------------------------------------- /examples/3/README.md: -------------------------------------------------------------------------------- 1 | # Example 3 2 | 3 | This example shows how you could use the braid protocol to do realtime 4 | collaborative editing using operational transform. 5 | 6 | Compared to example 2, this adds some new Stuff: 7 | 8 | - Versions. Each operation is versioned on the server 9 | - IDs. Each operation is assigned an ID in the client. These are used for 2 10 | reasons: 11 | - The server uses the ID for deduplication (in case messages get resent with 12 | bad internet) 13 | - The client uses the ID to detect (and discard) its own operations in the 14 | operation stream 15 | - OT. This example does full Operational Transformation in the server and 16 | client. 17 | 18 | This example is currently a sketch - most of the logic here is hairy and 19 | extremely difficult to implement correctly. Application authors probably 20 | shouldn't be doing it! This code should / will be tucked into another library. 21 | 22 | ## How to run this example 23 | 24 | Either compile and then run: 25 | 26 | ``` 27 | npx tsc # (Or npx tsc -w) 28 | node dist/server.js 29 | 30 | # and in another terminal 31 | node dist/client.js 32 | ``` 33 | 34 | Or use ts-node: 35 | 36 | ``` 37 | npx ts-node dist/server.js 38 | 39 | # and in another terminal 40 | npx ts-node dist/client.js 41 | ``` 42 | 43 | Each time you run the client it'll insert a new 'post' in the 'database'. 44 | -------------------------------------------------------------------------------- /examples/3/client.ts: -------------------------------------------------------------------------------- 1 | import { subscribeRaw } from '@braid-protocol/client-raw' 2 | import { JSONOp, type as json1, insertOp, Doc } from 'ot-json1' 3 | import { Post } from './shared' 4 | import makeStream, { Stream } from 'ministreamiterator' 5 | 6 | const decoder = new TextDecoder() 7 | 8 | interface StreamItem { 9 | value: T 10 | version: string 11 | op: JSONOp 12 | isLocal: boolean 13 | } 14 | 15 | const transformX = (op1: JSONOp, op2: JSONOp): [JSONOp, JSONOp] => [ 16 | json1.transformNoConflict(op1, op2, 'left'), 17 | json1.transformNoConflict(op2, op1, 'right'), 18 | ] 19 | 20 | const subscribeOT = async (url: string) => { 21 | const stream = makeStream>() 22 | 23 | const { streamHeaders, updates } = await subscribeRaw(url) 24 | // console.log('stream headers', streamHeaders) 25 | 26 | // The first value should contain the document itself. For now I'm just 27 | // hardcoding this - but this should deal correctly with known versions and 28 | // all that jazz. 29 | const first = await updates.next() 30 | if (first.done) throw Error('No messages in stream') 31 | 32 | // console.log('first headers', first.value.headers) 33 | if (first.value.type !== 'snapshot') throw Error('Expected subscription to start with a snapshot') 34 | let doc: T = JSON.parse(decoder.decode(first.value.value)) 35 | let serverVersion = first.value.headers['version'] 36 | 37 | // Operations waiting to be sent 38 | let pendingOp: JSONOp = null 39 | // Operations waiting to be acknowledged 40 | let inflightOp: { op: JSONOp; id: string } | null = null 41 | 42 | const processStream = async () => { 43 | for await (const data of updates) { 44 | const id = data.headers['patch-id'] 45 | 46 | if (inflightOp != null && id === inflightOp.id) { 47 | // Operation confirmed! 48 | inflightOp = null 49 | flushPending() 50 | } else { 51 | serverVersion = data.headers['version'] 52 | 53 | if (data.type === 'snapshot') { 54 | // Snapshot updates replace the contents of the document. Only 55 | // the first message in the subscription will be a snapshot 56 | // update here - though we may get them when reconnecting if 57 | // the server doesn't have context to catch up. 58 | 59 | // I'd implement it by replacing doc with the new value, but 60 | // we would also need to discard pending / inflight ops and 61 | // thats tricky. 62 | throw Error('Snapshot update inside the stream not supported') 63 | } else { 64 | // We'll only get one patch per message anyway, but eh. 65 | for (const {headers, body} of data.patches) { 66 | const patchType = headers['content-type'] 67 | ?? headers['patch-type'] 68 | ?? streamHeaders['patch-type'] 69 | if (patchType !== json1.name) throw Error('unsupported patch type') 70 | 71 | let op = JSON.parse(decoder.decode(body)) as JSONOp 72 | 73 | // Transform the incoming operation by any operations queued up to be 74 | // sent in the client. 75 | if (inflightOp != null) 76 | [inflightOp.op, op] = transformX(inflightOp.op, op) 77 | if (pendingOp != null) [pendingOp, op] = transformX(pendingOp, op) 78 | 79 | doc = json1.apply(doc as any, op) as any 80 | 81 | stream.append({ 82 | value: doc, 83 | version: serverVersion, 84 | op, 85 | isLocal: false, 86 | }) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | // This method is only called once anyway. I'd do it with ;(async () => {})() but for 93 | // some reason that confuses the TS typechecker. 94 | processStream() 95 | 96 | const sendInflight = async () => { 97 | // Could just ignore - but this should never happen. 98 | if (inflightOp == null) throw Error('Invalid call to sendInFlight') 99 | 100 | const res = await fetch(url, { 101 | method: 'PUT', 102 | headers: { 103 | 'patch-id': inflightOp.id, 104 | 'patch-type': json1.name, 105 | 'parents': serverVersion, 106 | 'content-type': 'application/json', 107 | }, 108 | body: JSON.stringify(inflightOp.op), 109 | }) 110 | 111 | console.log(await res.text()) 112 | 113 | // Ok - operation was acknowledged. Bump it. 114 | // inflightOp = null 115 | // flushPending() 116 | } 117 | 118 | const flushPending = () => { 119 | // We'll use only a single operation in-flight at once, to keep things a bit 120 | // simpler. 121 | if (inflightOp != null || pendingOp == null) return 122 | 123 | // Ok - set the pending operation in flight. 124 | inflightOp = { 125 | op: pendingOp, 126 | id: `${Math.random()}`.slice(2), 127 | } 128 | pendingOp = null 129 | sendInflight() 130 | } 131 | 132 | const submitChange = (op: JSONOp) => { 133 | doc = json1.apply(doc as any, op as any) as any 134 | pendingOp = json1.compose(pendingOp, op) 135 | 136 | stream.append({ 137 | value: doc, 138 | version: serverVersion, 139 | op, 140 | isLocal: true, 141 | }) 142 | 143 | flushPending() 144 | } 145 | 146 | return { 147 | patches: stream.iter, 148 | submitChange, 149 | initialValue: doc, 150 | initialVerson: serverVersion, 151 | } 152 | } 153 | 154 | ;(async () => { 155 | const { patches, submitChange, initialValue } = await subscribeOT( 156 | 'http://localhost:2003/doc' 157 | ) 158 | 159 | console.log('Connected. Initial document value', initialValue) 160 | 161 | // Submit an operation adding a new entry. 162 | const newEntry: Post = { title: 'hi', content: `${Math.random()}`.slice(2) } 163 | const op = insertOp([initialValue.length], newEntry as any) 164 | submitChange(op) 165 | 166 | // And stream changes to the console. 167 | for await (const data of patches) { 168 | console.log(data.isLocal ? 'Got local' : 'Got remote', 'op. New value:', data.value) 169 | } 170 | })() 171 | -------------------------------------------------------------------------------- /examples/3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example3", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@braid-protocol/client-raw": "*", 8 | "@braid-protocol/server": "*", 9 | "@types/body-parser": "^1.19.0", 10 | "@types/polka": "^0.5.2", 11 | "body-parser": "^1.19.0", 12 | "ministreamiterator": "^1.0.0", 13 | "ot-json1": "^1.0.1", 14 | "polka": "^0.5.2", 15 | "ts-node": "^9.1.1" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^14.14.22", 19 | "typescript": "^4.1.3" 20 | }, 21 | "scripts": { 22 | "prepare": "node -e '`examples/3: nothing to do`'" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/3/server.ts: -------------------------------------------------------------------------------- 1 | import polka from 'polka' 2 | import bodyParser from 'body-parser' 3 | import { stream, BraidStream } from '@braid-protocol/server' 4 | import { JSONOp, type as json1 } from 'ot-json1' 5 | import { Post } from './shared' 6 | 7 | let doc: Post[] = [] 8 | 9 | // The version is implied as the length of the history array. 10 | interface HistoryEntry { 11 | id?: string // Unique ID for dedup 12 | op: JSONOp 13 | } 14 | let history: HistoryEntry[] = [] 15 | 16 | // Set of clients to be updated. 17 | const clients = new Set() 18 | 19 | const applyPatch = ( 20 | op: JSONOp, 21 | version: number, 22 | patchId: string | undefined 23 | ) => { 24 | if (version > history.length) throw Error('Invalid version') 25 | 26 | while (version < history.length) { 27 | const entry = history[version] 28 | if (patchId != null && patchId === entry.id) return // Operation already applied. 29 | 30 | op = json1.transformNoConflict(op, entry.op, 'left') 31 | version++ 32 | } 33 | 34 | doc = json1.apply(doc as any, op) as any 35 | history.push({ 36 | id: patchId, 37 | op, 38 | }) 39 | 40 | console.log('applied change with id', patchId, 'doc is', doc) 41 | 42 | // And broadcast the operation to clients. 43 | for (const c of clients) { 44 | c.append({ 45 | patchId, 46 | version: `${version}`, 47 | patches: [ 48 | JSON.stringify(op) + '\n', 49 | ] 50 | }) 51 | } 52 | } 53 | 54 | const app = polka() 55 | 56 | app.get('/doc', (req, res) => { 57 | const s = stream(res, { 58 | reqHeaders: req.headers, 59 | initialValue: JSON.stringify(doc, null, 2) + '\n', 60 | initialVerson: `${history.length}`, 61 | contentType: 'application/json', 62 | patchType: json1.name, 63 | onclose() { 64 | if (s) clients.delete(s) 65 | }, 66 | }) 67 | if (s) clients.add(s) 68 | }) 69 | 70 | // You can test this with curl: 71 | // curl -XPUT -H'parents: 0' -H'patch-type: json1' -H'content-type: application/json' -d '[0,{"i":5}]' localhost:2003/doc 72 | app.put('/doc', bodyParser.json(), (req, res, next) => { 73 | const patchType = req.headers['patch-type'] 74 | if (patchType !== json1.name) 75 | return next(Error('Missing or unsupported patch type')) 76 | 77 | let parents = req.headers['parents'] 78 | if (parents == null || Array.isArray(parents)) 79 | return next(Error('Missing parents field')) 80 | parents = parents.trim() 81 | if (parents.startsWith('"')) parents = parents.slice(1, -1) // Trim off "" 82 | 83 | // The parents in this case will contain 1 item, a number, quoted like this: `Parents: "123"` 84 | const version = parseInt(parents) 85 | if (isNaN(version)) return next(Error('Invalid parents field')) 86 | 87 | const op = req.body 88 | const opId = req.headers['patch-id'] 89 | 90 | console.log('Received operation', op, 'id:', opId) 91 | 92 | try { 93 | applyPatch(op, version, opId as string | undefined) 94 | } catch (e) { next(e) } 95 | 96 | // The version header will be ignored. We don't care. 97 | res.end() 98 | }) 99 | 100 | app.listen(2003, (err: Error) => { 101 | if (err) throw err 102 | console.log('listening on http://localhost:2003/doc') 103 | }) 104 | -------------------------------------------------------------------------------- /examples/3/shared.ts: -------------------------------------------------------------------------------- 1 | export interface Post { 2 | title: string 3 | content: string 4 | } 5 | 6 | export type Doc = Post[] 7 | -------------------------------------------------------------------------------- /examples/3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "client-raw", 5 | "client", 6 | "server", 7 | "cli", 8 | "examples/*" 9 | ], 10 | "scripts": { 11 | "prepare": "yarn workspaces run prepare" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Braid protocol server 2 | 3 | This is a simple reference implementation of the [braid](https://github.com/braid-org/braid-spec/) protocol for nodejs. 4 | 5 | Usage: 6 | 7 | ``` 8 | npm i --save josephg/braid-server 9 | ``` 10 | 11 | The braid server works to stream a series of values or patches to any client using any nodejs response object. This is compatible with express / connect / polka / etc. Eg, with polka: 12 | 13 | ```javascript 14 | const polka = require('polka') 15 | const sirv = require('sirv') 16 | const cors = require('cors') 17 | const braid = require('@braid-protocol/server') 18 | 19 | const assets = sirv(__dirname + '/web') 20 | 21 | const getDate = () => new Date().toLocaleString() + '\n' 22 | 23 | polka() 24 | .options('/time', cors({methods: ['GET']})) 25 | .get('/time', cors(), (req, res) => { 26 | let timer 27 | 28 | if (req.headers.subscribe === 'keep-alive') { 29 | const stream = braid.stream(res, { 30 | initialValue: getDate(), 31 | contentType: 'text/plain', 32 | onclose() { 33 | clearInterval(timer) 34 | }, 35 | }) 36 | 37 | timer = setInterval(() => { 38 | stream.append({ value: getDate() }) 39 | }, 1000) 40 | } else { 41 | res.end(getDate()) 42 | } 43 | }) 44 | .use(assets) 45 | .listen(2001, (err) => { 46 | if (err) throw err 47 | console.log('listening on http://localhost:2001/time') 48 | console.log('Open http://localhost:2001/ in a browser for a simple demo') 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@braid-protocol/server", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "public": true, 7 | "license": "ISC", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/josephg/braid-protocol" 11 | }, 12 | "dependencies": { 13 | "ministreamiterator": "^1.0.0" 14 | }, 15 | "scripts": { 16 | "dev": "tsc --watch --preserveWatchOutput", 17 | "prepare": "tsc" 18 | }, 19 | "files": [ 20 | "dist/*" 21 | ], 22 | "devDependencies": { 23 | "@types/node": "^14.14.21", 24 | "ot-fuzzer": "^1.3.0", 25 | "ot-text-unicode": "^4.0.0", 26 | "typescript": "^4.1.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/src/StringLike.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephg/braid-protocol/9e861c5c98d6be1c8db100d1df7f84a56af53df3/server/src/StringLike.ts -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | export { stream } from './stream' 2 | export { PatchUpdate, SnapshotUpdate, BraidStream, StringOrBuf as StringLike } from './types' 3 | -------------------------------------------------------------------------------- /server/src/stream.ts: -------------------------------------------------------------------------------- 1 | import asyncstream from 'ministreamiterator' 2 | import { ServerResponse } from 'http' 3 | import { Update, PatchUpdate, SnapshotUpdate, BraidStream, StringOrBuf, Patch } from './types' 4 | 5 | const DEFAULT_HEARTBEAT_SECS = 30 6 | 7 | const toBuf = (data: StringOrBuf): Buffer => ( 8 | // Actually Buffer.from(data, 'utf-8') would be fine here but 9 | // typescript doesn't like it. Eh. 10 | Buffer.isBuffer(data) ? data 11 | : typeof data === 'string' ? Buffer.from(data, 'utf-8') 12 | : Buffer.from(data) 13 | ) 14 | 15 | const isStringOrBuf = (val: any): val is StringOrBuf => ( 16 | typeof val === 'string' 17 | || Buffer.isBuffer(val) 18 | || val instanceof Uint8Array 19 | ) 20 | 21 | interface StateServerOpts { 22 | /** 23 | * Optional headers from request, so we can parse out requested patch type 24 | * and requested version 25 | */ 26 | reqHeaders?: NodeJS.Dict 27 | 28 | initialVerson?: string 29 | initialValue?: StringOrBuf 30 | 31 | /** HTTP content-type for the resource */ 32 | contentType?: string 33 | 34 | /** 35 | * Most servers will use a single patch type everywhere. See 36 | * https://github.com/braid-org/braid-spec/issues/106 37 | * 38 | * If this is set, the patchType in each actual patch is assumed to be 39 | * this value. For now this library will still send the patch type 40 | * with every patch for compatibility. 41 | */ 42 | patchType?: 'braid' | 'merge-object' | string 43 | // encodePatch?: (patch: any) => string | Buffer, 44 | 45 | httpHeaders?: { [k: string]: string | any } 46 | 47 | /** 48 | * Optional event handler called when the peer disconnects 49 | */ 50 | onclose?: () => void 51 | 52 | /** 53 | * Send a heartbeat message every (seconds). Defaults to every 30 54 | * seconds. This is needed to avoid some browsers (Firefox) closing 55 | * the connection automatically after a 1 minute timeout. 56 | * 57 | * Set to `null` to disable heartbeat messages. 58 | */ 59 | heartbeatSecs?: number | null 60 | } 61 | 62 | export interface MaybeFlushable { 63 | flush?: () => void 64 | } 65 | 66 | const headersToBuf = (headers: Record) => ( 67 | Buffer.from( 68 | Object.entries(headers) 69 | .map(([k, v]) => `${k}: ${v}\r\n`) 70 | .join('') + '\r\n' 71 | ) 72 | ) 73 | 74 | /* 75 | 76 | Switches: 77 | 78 | - Are we sending snapshots or are we sending patches? 79 | - When is the client up-to-date? 80 | 81 | - The client can request changes from some specified version 82 | - And the updates can name a version 83 | 84 | - Patch type can change per message (according to the current braid spec) 85 | 86 | - Client can send accepts-patch header to name which patch types it understands 87 | 88 | */ 89 | 90 | function sendInitialValOnly(res: ServerResponse, opts: StateServerOpts): void { 91 | // TODO: Not actually sure what we should do in this case. 92 | if (!opts.initialValue) { 93 | throw Error( 94 | 'Cannot send a single value to a client that does not subscribe' 95 | ) 96 | } 97 | 98 | const httpHeaders = { 99 | ...opts.httpHeaders, 100 | } 101 | 102 | if (opts.contentType) httpHeaders['content-type'] = opts.contentType 103 | if (opts.initialVerson) httpHeaders['version'] = opts.initialVerson 104 | 105 | let bufData = toBuf(opts.initialValue) 106 | httpHeaders['content-length'] = bufData.length 107 | 108 | res.writeHead(200, 'OK', httpHeaders) 109 | res.end(bufData) 110 | 111 | if (opts.onclose) process.nextTick(opts.onclose) 112 | } 113 | 114 | const updateIsSnapshot = (upd: Update): upd is SnapshotUpdate => ( 115 | (upd as any).patches === undefined 116 | ) 117 | 118 | export function stream( 119 | res: ServerResponse & MaybeFlushable, 120 | opts: StateServerOpts = {} 121 | ): BraidStream | void { 122 | // These headers are sent both in the HTTP response and in the first SSE 123 | // message, because there's no API for reading these headers back from 124 | // EventSource in the browser. 125 | 126 | // If the client did not request a subscription, we'll just pass them the 127 | // initial value and bail. 128 | if (opts.reqHeaders && opts.reqHeaders['subscribe'] !== 'keep-alive') { 129 | return sendInitialValOnly(res, opts) 130 | } 131 | 132 | const httpHeaders: Record = { 133 | 'cache-control': 'no-cache', 134 | 'connection': 'keep-alive', 135 | ...opts.httpHeaders, 136 | } 137 | 138 | let contentType = opts.contentType ?? null 139 | if (contentType != null) httpHeaders['content-type'] = contentType 140 | 141 | // As per https://github.com/braid-org/braid-spec/issues/106 142 | if (opts.patchType) httpHeaders['patch-type'] = opts.patchType 143 | 144 | res.writeHead(209, 'Subscription', httpHeaders) 145 | 146 | let connected = true 147 | 148 | const stream = asyncstream(() => { 149 | connected = false 150 | res.end() // will fire res.emit('close') if not already closed 151 | }) 152 | 153 | res.once('close', () => { 154 | connected = false 155 | stream.end() 156 | opts.onclose?.() 157 | }) 158 | 159 | // Using newline heartbeats here. 160 | // https://github.com/braid-org/braid-spec/issues/104 161 | if (opts.heartbeatSecs !== null) { 162 | const heartbeatSecs = opts.heartbeatSecs ?? DEFAULT_HEARTBEAT_SECS 163 | ;(async () => { 164 | // 30 second heartbeats to avoid timeouts 165 | while (true) { 166 | await new Promise(res => setTimeout(res, heartbeatSecs * 1000)) 167 | 168 | if (!connected) break 169 | 170 | res.write(`\n`); 171 | res.flush?.() 172 | } 173 | })() 174 | } 175 | 176 | ;(async () => { 177 | if (connected) { 178 | for await (const upd of stream.iter) { 179 | if (!connected) break 180 | 181 | // This is the "2nd tier" of headers, i.e. after the HTTP headers, there 182 | // are "Update" headers, which can include a "Version:" header. 183 | let updateHeaders: Record = { ...upd.headers } 184 | 185 | // Whether we have patches or just a version, include the version here 186 | if (upd.version != null) updateHeaders['version'] = upd.version 187 | 188 | // This is not in the spec (yet). 189 | if (upd.patchId != null) updateHeaders['patch-id'] = upd.patchId 190 | 191 | if (updateIsSnapshot(upd)) { 192 | const valueBuf = toBuf(upd.value) 193 | updateHeaders['content-length'] = `${valueBuf.length}` 194 | res.write(Buffer.concat([ 195 | headersToBuf(updateHeaders), 196 | valueBuf, 197 | // Braidjs wants a newline here. 198 | // Buffer.from('\n') 199 | ])) 200 | } else { 201 | // Sending a set of patches 202 | updateHeaders['patches'] = `${upd.patches.length}` 203 | 204 | // I'm building up a list of message buffers and I'll send 205 | // them in one res.write() call to cut down on the number of 206 | // transfer-encoding chunks being sent. This might slightly 207 | // lower performance - I'm not sure if that matters here. 208 | const messages = [headersToBuf(updateHeaders)] 209 | 210 | for (let patch of upd.patches) { 211 | // This is a bit gross. We allow patches to be specified as 212 | // using their raw buffer / string instead of needing to be 213 | // wrapped. This is simple but allocation-inefficient. 214 | if (isStringOrBuf(patch)) patch = {body: patch} 215 | const {range} = patch 216 | 217 | const patchType = patch.patchType 218 | ?? (range != null ? 'braid' : null) 219 | ?? opts.patchType 220 | 221 | if (patchType == null) { 222 | throw Error('Cannot infer type of patch inside update. Set patch.patchType, patch.range or connection-global opts.patchType.') 223 | } 224 | 225 | let bodyBuf = toBuf(patch.body) 226 | const patchHeaders: Record = { 227 | ...patch.headers, 228 | 'content-length': `${bodyBuf.length}`, 229 | } 230 | 231 | if (range) patchHeaders['content-range'] = range 232 | 233 | if (patchType === 'braid') { 234 | if (range == null) throw Error('Invalid braid patch - expected range header') 235 | patchHeaders['content-range'] = range 236 | } else { 237 | // TODO: Remove this after issue #106 resolves when the 238 | // patch is set globally. Also note this will probably be 239 | // renamed to patch-type in a future version of the spec. 240 | patchHeaders['content-type'] = patchType 241 | } 242 | 243 | messages.push(headersToBuf(patchHeaders)) 244 | messages.push(bodyBuf) 245 | } 246 | res.write(Buffer.concat(messages)) 247 | 248 | } 249 | res.flush?.() 250 | } 251 | } 252 | })() 253 | 254 | if (opts.initialValue !== undefined) { 255 | stream.append({ 256 | value: opts.initialValue, 257 | version: opts.initialVerson, 258 | }) 259 | } 260 | 261 | return stream 262 | } 263 | -------------------------------------------------------------------------------- /server/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'ministreamiterator' 2 | 3 | export type StringOrBuf = string | Buffer | Uint8Array 4 | 5 | // export function toBuf(data: StringLike): Buffer { 6 | // return typeof data === 'string' ? Buffer.from(data, 'utf8') : data 7 | // } 8 | 9 | type CommonUpdate = { 10 | /** 11 | * Version of the operation. The version must be unique to all versions in 12 | * this stream. 13 | * 14 | * This may be assigned by the server or by a client, depending on how braid 15 | * is used. 16 | */ 17 | version?: string 18 | 19 | /** 20 | * Used for dedup and client matching when version is server-assigned. (Eg in 21 | * OT). Not in the spec. 22 | */ 23 | patchId?: string 24 | 25 | /** 26 | * Additional headers attached to this message when it is broadcast to clients 27 | */ 28 | headers?: Record 29 | } 30 | 31 | export type SnapshotUpdate = CommonUpdate & { 32 | /** The encoded contents of the new document snapshot */ 33 | value: StringOrBuf 34 | } 35 | 36 | export type Patch = { 37 | headers?: Record 38 | /** Defaults to the patch type of the stream. At least one or the other must be set. */ 39 | patchType?: string 40 | /** Used for braid patches. Implies patchType = 'braid' */ 41 | range?: string 42 | body: StringOrBuf 43 | } 44 | 45 | export type PatchUpdate = CommonUpdate & { 46 | // The patches can just be specified as strings / buffers instead of 47 | // full patch objects, if the patch type is specified at the top 48 | // level. 49 | patches: Array 50 | } 51 | 52 | export type Update = SnapshotUpdate | PatchUpdate 53 | export type BraidStream = Stream 54 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | } 70 | } 71 | --------------------------------------------------------------------------------