diff --git a/pkg/npm/http-api/example/browser.js b/pkg/npm/http-api/example/browser.js index 9ec2eea57f..9af8e1189b 100644 --- a/pkg/npm/http-api/example/browser.js +++ b/pkg/npm/http-api/example/browser.js @@ -1,17 +1,32 @@ /* * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). - * This devtool is not neither made for production nor for readable output files. + * This devtool is neither made for production nor for readable output files. * It uses "eval()" calls to create a separate source file in the browser devtools. * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) * or disable the default devtool with "devtool: false". * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). */ /******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ "./src/example/browser.js": /*!********************************!*\ !*** ./src/example/browser.js ***! \********************************/ -/*! unknown exports (runtime-defined) */ -/*! runtime requirements: */ +/***/ (() => { + eval("// import Urbit from '../../dist/browser';\n// window.Urbit = Urbit;\n\n//# sourceURL=webpack://@urbit/http-api/./src/example/browser.js?"); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ +/******/ // startup +/******/ // Load entry module and return exports +/******/ // This entry module can't be inlined because the eval devtool is used. +/******/ var __webpack_exports__ = {}; +/******/ __webpack_modules__["./src/example/browser.js"](); +/******/ /******/ })() ; \ No newline at end of file diff --git a/pkg/npm/http-api/example/node.js b/pkg/npm/http-api/example/node.js index 4dc1b5b034..3d572dd70c 100644 --- a/pkg/npm/http-api/example/node.js +++ b/pkg/npm/http-api/example/node.js @@ -1,17 +1,32 @@ /* * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). - * This devtool is not neither made for production nor for readable output files. + * This devtool is neither made for production nor for readable output files. * It uses "eval()" calls to create a separate source file in the browser devtools. * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) * or disable the default devtool with "devtool: false". * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). */ /******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ "./src/example/node.js": /*!*****************************!*\ !*** ./src/example/node.js ***! \*****************************/ -/*! unknown exports (runtime-defined) */ -/*! runtime requirements: */ +/***/ (() => { + eval("// import Urbit from '../../dist/index';\n// async function blastOff() {\n// const airlock = await Urbit.authenticate({\n// ship: 'zod',\n// url: 'localhost:8080',\n// code: 'lidlut-tabwed-pillex-ridrup',\n// verbose: true\n// });\n// airlock.subscribe('chat-view', '/primary');\n// }\n// blastOff();\n\n//# sourceURL=webpack://@urbit/http-api/./src/example/node.js?"); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ +/******/ // startup +/******/ // Load entry module and return exports +/******/ // This entry module can't be inlined because the eval devtool is used. +/******/ var __webpack_exports__ = {}; +/******/ __webpack_modules__["./src/example/node.js"](); +/******/ /******/ })() ; \ No newline at end of file diff --git a/pkg/npm/http-api/index.js b/pkg/npm/http-api/index.js deleted file mode 100644 index 351a32475c..0000000000 --- a/pkg/npm/http-api/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import Urbit from './dist'; -export { Urbit as default, Urbit }; \ No newline at end of file diff --git a/pkg/npm/http-api/package.json b/pkg/npm/http-api/package.json index 25d701a284..ca756dc1bc 100644 --- a/pkg/npm/http-api/package.json +++ b/pkg/npm/http-api/package.json @@ -8,20 +8,15 @@ "url": "ssh://git@github.com/urbit/urbit.git", "directory": "pkg/npm/http-api" }, - "main": "dist/cjs/index.js", - "module": "dist/esm/index.js", - "browser": "dist/esm/index.js", - "types": "dist/esm/index.d.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", "files": [ "dist", "src" ], - "engines": { - "node": ">=13" - }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "npm run clean && webpack --config webpack.prod.js && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json", + "build": "npm run clean && tsc -p tsconfig.json", "clean": "rm -rf dist/*" }, "peerDependencies": {}, @@ -38,31 +33,29 @@ "@babel/plugin-proposal-object-rest-spread": "^7.12.1", "@babel/plugin-proposal-optional-chaining": "^7.12.1", "@babel/preset-typescript": "^7.12.1", + "@types/browser-or-node": "^1.2.0", "@types/eventsource": "^1.1.5", "@types/react": "^16.9.56", "@typescript-eslint/eslint-plugin": "^4.7.0", "@typescript-eslint/parser": "^4.7.0", - "@types/browser-or-node": "^1.2.0", "babel-loader": "^8.2.1", "clean-webpack-plugin": "^3.0.0", "tslib": "^2.0.3", "typescript": "^3.9.7", + "util": "^0.12.3", "webpack": "^5.4.0", "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0" }, "dependencies": { "@babel/runtime": "^7.12.5", + "@microsoft/fetch-event-source": "^2.0.0", + "@urbit/api": "file:../api", "browser-or-node": "^1.3.0", "browserify-zlib": "^0.2.0", - "buffer": "^5.7.1", - "encoding": "^0.1.13", - "eventsource": "^1.0.7", + "buffer": "^6.0.3", "node-fetch": "^2.6.1", "stream-browserify": "^3.0.0", - "stream-http": "^3.1.1", - "util": "^0.12.3", - "xmlhttprequest": "^1.8.0", - "xmlhttprequest-ssl": "^1.6.0" + "stream-http": "^3.1.1" } } diff --git a/pkg/npm/http-api/src/Urbit.ts b/pkg/npm/http-api/src/Urbit.ts new file mode 100644 index 0000000000..4f01361683 --- /dev/null +++ b/pkg/npm/http-api/src/Urbit.ts @@ -0,0 +1,372 @@ +import { isBrowser, isNode } from 'browser-or-node'; +import { Action, Scry, Thread } from '@urbit/api'; +import { fetchEventSource, EventSourceMessage } from '@microsoft/fetch-event-source'; + +import { AuthenticationInterface, SubscriptionInterface, CustomEventHandler, PokeInterface, SubscriptionRequestInterface, headers, UrbitInterface, SSEOptions, PokeHandlers } from './types'; +import { uncamelize, hexString } from './utils'; + +/** + * A class for interacting with an urbit ship, given its URL and code + */ +export class Urbit implements UrbitInterface { + /** + * UID will be used for the channel: The current unix time plus a random hex string + */ + uid: string = `${Math.floor(Date.now() / 1000)}-${hexString(6)}`; + + /** + * Last Event ID is an auto-updated index of which events have been sent over this channel + */ + lastEventId: number = 0; + + lastAcknowledgedEventId: number = 0; + + /** + * SSE Client is null for now; we don't want to start polling until it the channel exists + */ + sseClientInitialized: boolean = false; + + /** + * Cookie gets set when we log in. + */ + cookie?: string | undefined; + + /** + * A registry of requestId to successFunc/failureFunc + * + * These functions are registered during a +poke and are executed + * in the onServerEvent()/onServerError() callbacks. Only one of + * the functions will be called, and the outstanding poke will be + * removed after calling the success or failure function. + */ + + outstandingPokes: Map = new Map(); + + /** + * A registry of requestId to subscription functions. + * + * These functions are registered during a +subscribe and are + * executed in the onServerEvent()/onServerError() callbacks. The + * event function will be called whenever a new piece of data on this + * subscription is available, which may be 0, 1, or many times. The + * disconnect function may be called exactly once. + */ + + outstandingSubscriptions: Map = new Map(); + + /** + * Ship can be set, in which case we can do some magic stuff like send chats + */ + ship?: string | null; + + /** + * If verbose, logs output eagerly. + */ + verbose?: boolean; + + /** This is basic interpolation to get the channel URL of an instantiated Urbit connection. */ + get channelUrl(): string { + return `${this.url}/~/channel/${this.uid}`; + } + + get fetchOptions(): any { + const headers: headers = { + 'Content-Type': 'application/json', + }; + if (!isBrowser) { + headers.Cookie = this.cookie; + } + return { + credentials: 'include', + headers + }; + } + + /** + * Constructs a new Urbit connection. + * + * @param url The URL (with protocol and port) of the ship to be accessed + * @param code The access code for the ship at that address + */ + constructor( + public url: string, + public code: string + ) { + return this; + } + + /** + * All-in-one hook-me-up. + * + * Given a ship, url, and code, this returns an airlock connection + * that is ready to go. It `|hi`s itself to create the channel, + * then opens the channel via EventSource. + * + * @param AuthenticationInterface + */ + static async authenticate({ ship, url, code, verbose = false }: AuthenticationInterface) { + const airlock = new Urbit(`http://${url}`, code); + airlock.verbose = verbose; + airlock.ship = ship; + await airlock.connect(); + await airlock.poke({ app: 'hood', mark: 'helm-hi', json: 'opening airlock' }); + await airlock.eventSource(); + return airlock; + } + + /** + * Connects to the Urbit ship. Nothing can be done until this is called. + * That's why we roll it into this.authenticate + */ + async connect(): Promise { + if (this.verbose) { + console.log(`password=${this.code} `, isBrowser ? "Connecting in browser context at " + `${this.url}/~/login` : "Connecting from node context"); + } + return fetch(`${this.url}/~/login`, { + method: 'post', + body: `password=${this.code}`, + credentials: 'include', + }).then(response => { + if (this.verbose) { + console.log('Received authentication response', response); + } + const cookie = response.headers.get('set-cookie'); + if (!this.ship) { + this.ship = new RegExp(/urbauth-~([\w-]+)/).exec(cookie)[1]; + } + if (!isBrowser) { + this.cookie = cookie; + } + }); + } + + + /** + * Initializes the SSE pipe for the appropriate channel. + */ + eventSource(): void{ + if (!this.sseClientInitialized) { + const sseOptions: SSEOptions = { + headers: {} + }; + if (isBrowser) { + sseOptions.withCredentials = true; + } else if (isNode) { + sseOptions.headers.Cookie = this.cookie; + } + fetchEventSource(this.channelUrl, { + // withCredentials: true, + onmessage: (event: EventSourceMessage) => { + if (this.verbose) { + console.log('Received SSE: ', event); + } + this.ack(Number(event.id)); + if (event.data && JSON.parse(event.data)) { + const data: any = JSON.parse(event.data); + if (data.response === 'poke' && this.outstandingPokes.has(data.id)) { + const funcs = this.outstandingPokes.get(data.id); + if (data.hasOwnProperty('ok')) { + funcs.onSuccess(); + } else if (data.hasOwnProperty('err')) { + funcs.onError(data.err); + } else { + console.error('Invalid poke response', data); + } + this.outstandingPokes.delete(data.id); + } else if (data.response === 'subscribe' || + (data.response === 'poke' && this.outstandingSubscriptions.has(data.id))) { + const funcs = this.outstandingSubscriptions.get(data.id); + if (data.hasOwnProperty('err')) { + funcs.err(data.err); + this.outstandingSubscriptions.delete(data.id); + } + } else if (data.response === 'diff' && this.outstandingSubscriptions.has(data.id)) { + const funcs = this.outstandingSubscriptions.get(data.id); + funcs.event(data.json); + } else if (data.response === 'quit' && this.outstandingSubscriptions.has(data.id)) { + const funcs = this.outstandingSubscriptions.get(data.id); + funcs.quit(data); + this.outstandingSubscriptions.delete(data.id); + } else { + console.log('Unrecognized response', data); + } + } + }, + onerror: (error) => { + console.error('pipe error', error); + } + }); + this.sseClientInitialized = true; + } + return; + } + + /** + * Autoincrements the next event ID for the appropriate channel. + */ + getEventId(): number { + this.lastEventId = Number(this.lastEventId) + 1; + return this.lastEventId; + } + + /** + * Acknowledges an event. + * + * @param eventId The event to acknowledge. + */ + ack(eventId: number): Promise { + return this.sendMessage('ack', { 'event-id': eventId }); + } + + /** + * This is a wrapper method that can be used to send any action with data. + * + * Every message sent has some common parameters, like method, headers, and data + * structure, so this method exists to prevent duplication. + * + * @param action The action to send + * @param data The data to send with the action + * + * @returns void | number If successful, returns the number of the message that was sent + */ + async sendMessage(action: Action, data?: object): Promise { + + const id = this.getEventId(); + if (this.verbose) { + console.log(`Sending message ${id}:`, action, data,); + } + let response: Response | undefined; + try { + response = await fetch(this.channelUrl, { + ...this.fetchOptions, + method: 'put', + body: JSON.stringify([{ + id, + action, + ...data, + }]), + }); + } catch (error) { + console.error('message error', error); + response = undefined; + } + if (this.verbose) { + console.log(`Received from message ${id}: `, response); + } + return id; + } + + /** + * Pokes a ship with data. + * + * @param app The app to poke + * @param mark The mark of the data being sent + * @param json The data to send + */ + poke(params: PokeInterface): Promise { + const { app, mark, json, onSuccess, onError } = { onSuccess: () => {}, onError: () => {}, ...params }; + return new Promise((resolve, reject) => { + this + .sendMessage('poke', { ship: this.ship, app, mark, json }) + .then(pokeId => { + if (!pokeId) { + return reject('Poke failed'); + } + if (!this.sseClientInitialized) resolve(pokeId); // A poke may occur before a listener has been opened + this.outstandingPokes.set(pokeId, { + onSuccess: () => { + onSuccess(); + resolve(pokeId); + }, + onError: (event) => { + onError(event); + reject(event.err); + } + }); + }).catch(error => { + console.error(error); + }); + }); + } + + /** + * Subscribes to a path on an app on a ship. + * + * @param app The app to subsribe to + * @param path The path to which to subscribe + * @param handlers Handlers to deal with various events of the subscription + */ + async subscribe(params: SubscriptionRequestInterface): Promise { + const { app, path, err, event, quit } = { err: () => {}, event: () => {}, quit: () => {}, ...params }; + + const subscriptionId = await this.sendMessage('subscribe', { ship: this.ship, app, path }); + + if (!subscriptionId) return; + + this.outstandingSubscriptions.set(subscriptionId, { + err, event, quit + }); + + return subscriptionId; + } + + /** + * Unsubscribes to a given subscription. + * + * @param subscription + */ + unsubscribe(subscription: string): Promise { + return this.sendMessage('unsubscribe', { subscription }); + } + + /** + * Deletes the connection to a channel. + */ + delete(): Promise { + return this.sendMessage('delete'); + } + + /** + * + * @param app The app into which to scry + * @param path The path at which to scry + */ + async scry(params: Scry): Promise { + const { app, path } = params; + const response = await fetch(`/~/scry/${app}${path}.json`, this.fetchOptions); + return await response.json(); + } + + /** + * + * @param inputMark The mark of the data being sent + * @param outputMark The mark of the data being returned + * @param threadName The thread to run + * @param body The data to send to the thread + */ + async thread(params: Thread): Promise { + const { inputMark, outputMark, threadName, body } = params; + const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, { + ...this.fetchOptions, + method: 'POST', + body: JSON.stringify(body) + }); + + return res.json(); + } + + /** + * Utility function to connect to a ship that has its *.arvo.network domain configured. + * + * @param name Name of the ship e.g. zod + * @param code Code to log in + */ + static async onArvoNetwork(ship: string, code: string): Promise { + const url = `https://${ship}.arvo.network`; + return await Urbit.authenticate({ ship, url, code }); + } +} + + + +export default Urbit; diff --git a/pkg/npm/http-api/src/app/base.ts b/pkg/npm/http-api/src/app/base.ts deleted file mode 100644 index 715886b515..0000000000 --- a/pkg/npm/http-api/src/app/base.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Urbit from '..'; - -export interface UrbitAppInterface { - airlock: Urbit; - app: string; -} - -export default class UrbitApp implements UrbitAppInterface { - airlock: Urbit; - - get app(): string { - throw new Error('Access app property on base UrbitApp'); - } - - constructor(airlock: Urbit) { - this.airlock = airlock; - } - - /** - * Getter that barfs if no ship has been passed - */ - get ship(): string { - if (!this.airlock.ship) { - throw new Error('No ship specified'); - } - return this.airlock.ship; - } - - /** - * Helper to allow any app to handle subscriptions. - * - * @param path Path on app to subscribe to - */ - subscribe(path: string) { - const ship = this.ship; - const app = this.app; - // @ts-ignore - return this.airlock.subscribe(app, path); - } - // TODO handle methods that don't exist -} diff --git a/pkg/npm/http-api/src/index.ts b/pkg/npm/http-api/src/index.ts index b08f46e532..8e242ef6b0 100644 --- a/pkg/npm/http-api/src/index.ts +++ b/pkg/npm/http-api/src/index.ts @@ -1,457 +1,3 @@ -import { isBrowser, isNode } from 'browser-or-node'; -import { Action, Thread } from '../../api'; - -import { AuthenticationInterface, SubscriptionInterface, CustomEventHandler, PokeInterface, SubscriptionRequestInterface, headers, UrbitInterface, SSEOptions, PokeHandlers } from './types'; -import UrbitApp from './app/base'; -import { uncamelize, hexString } from './utils'; - -/** - * A class for interacting with an urbit ship, given its URL and code - */ -export class Urbit implements UrbitInterface { - /** - * UID will be used for the channel: The current unix time plus a random hex string - */ - uid: string = `${Math.floor(Date.now() / 1000)}-${hexString(6)}`; - - /** - * Last Event ID is an auto-updated index of which events have been sent over this channel - */ - lastEventId: number = 0; - - lastAcknowledgedEventId: number = 0; - - /** - * SSE Client is null for now; we don't want to start polling until it the channel exists - */ - sseClient: EventSource | null = null; - - /** - * Cookie gets set when we log in. - */ - cookie?: string | undefined; - - /** - * A registry of requestId to successFunc/failureFunc - * - * These functions are registered during a +poke and are executed - * in the onServerEvent()/onServerError() callbacks. Only one of - * the functions will be called, and the outstanding poke will be - * removed after calling the success or failure function. - */ - - outstandingPokes: Map = new Map(); - - /** - * A registry of requestId to subscription functions. - * - * These functions are registered during a +subscribe and are - * executed in the onServerEvent()/onServerError() callbacks. The - * event function will be called whenever a new piece of data on this - * subscription is available, which may be 0, 1, or many times. The - * disconnect function may be called exactly once. - */ - - outstandingSubscriptions: Map = new Map(); - - /** - * Ship can be set, in which case we can do some magic stuff like send chats - */ - ship?: string | null; - - /** - * If verbose, logs output eagerly. - */ - verbose?: boolean; - - /** - * All registered apps, keyed by name - */ - static apps: Map = new Map(); - - /** This is basic interpolation to get the channel URL of an instantiated Urbit connection. */ - get channelUrl(): string { - return `${this.url}/~/channel/${this.uid}`; - } - - get fetchOptions(): any { - const headers: headers = { - 'Content-Type': 'application/json', - }; - if (!isBrowser) { - headers.Cookie = this.cookie; - } - return { - credentials: 'include', - headers - }; - } - - /** - * Constructs a new Urbit connection. - * - * @param url The URL (with protocol and port) of the ship to be accessed - * @param code The access code for the ship at that address - */ - constructor( - public url: string, - public code: string - ) { - return this; - // We return a proxy so we can set dynamic properties like `Urbit.onChatHook` - // @ts-ignore - return new Proxy(this, { - get(target: Urbit, property: string) { - // First check if this is a regular property - if (property in target) { - return (target as any)[property]; - } - - // Then check if it's a registered app - const app = Urbit.apps.get(uncamelize(property)); - if (app) { - return new app(target); - } - - // Then check to see if we're trying to register an EventSource watcher - if (property.startsWith('on')) { - const on = uncamelize(property.replace('on', '')).toLowerCase(); - return ((action: CustomEventHandler) => { - target.eventSource().addEventListener('message', (event: MessageEvent) => { - if (target.verbose) { - console.log(`Received SSE from ${on}: `, event); - } - if (event.data && JSON.parse(event.data)) { - const data: any = JSON.parse(event.data); - if (data.json.hasOwnProperty(on)) { - action(data.json[on], data.json.response); - } - } - }); - }); - } - - return undefined; - } - }) - } - - /** - * All-in-one hook-me-up. - * - * Given a ship, url, and code, this returns an airlock connection - * that is ready to go. It `|hi`s itself to create the channel, - * then opens the channel via EventSource. - * - * @param AuthenticationInterface - */ - static async authenticate({ ship, url, code, verbose = false }: AuthenticationInterface) { - const airlock = new Urbit(`http://${url}`, code); - airlock.verbose = verbose; - airlock.ship = ship; - await airlock.connect(); - await airlock.poke({ app: 'hood', mark: 'helm-hi', json: 'opening airlock' }); - await airlock.eventSource(); - return airlock; - } - - /** - * Connects to the Urbit ship. Nothing can be done until this is called. - * That's why we roll it into this.authenticate - */ - async connect(): Promise { - if (this.verbose) { - console.log(`password=${this.code} `, isBrowser ? "Connecting in browser context at " + `${this.url}/~/login` : "Connecting from node context"); - } - return fetch(`${this.url}/~/login`, { - method: 'post', - body: `password=${this.code}`, - credentials: 'include', - }).then(response => { - if (this.verbose) { - console.log('Received authentication response', response); - } - const cookie = response.headers.get('set-cookie'); - if (!this.ship) { - this.ship = new RegExp(/urbauth-~([\w-]+)/).exec(cookie)[1]; - } - if (!isBrowser) { - this.cookie = cookie; - } - }).catch(error => { - console.log(XMLHttpRequest); - console.log('errored') - console.log(error); - }); - } - - - /** - * Returns (and initializes, if necessary) the SSE pipe for the appropriate channel. - */ - eventSource(): EventSource { - if (!this.sseClient || this.sseClient.readyState === this.sseClient.CLOSED) { - const sseOptions: SSEOptions = { - headers: {} - }; - if (isBrowser) { - sseOptions.withCredentials = true; - } else if (isNode) { - sseOptions.headers.Cookie = this.cookie; - } - this.sseClient = new EventSource(this.channelUrl, { - withCredentials: true - }); - this.sseClient!.addEventListener('message', (event: MessageEvent) => { - if (this.verbose) { - console.log('Received SSE: ', event); - } - this.ack(Number(event.lastEventId)); - if (event.data && JSON.parse(event.data)) { - const data: any = JSON.parse(event.data); - if (data.response === 'poke' && this.outstandingPokes.has(data.id)) { - const funcs = this.outstandingPokes.get(data.id); - if (data.hasOwnProperty('ok')) { - funcs.onSuccess(); - } else if (data.hasOwnProperty('err')) { - funcs.onError(data.err); - } else { - console.error('Invalid poke response', data); - } - this.outstandingPokes.delete(data.id); - } else if (data.response === 'subscribe' || - (data.response === 'poke' && this.outstandingSubscriptions.has(data.id))) { - const funcs = this.outstandingSubscriptions.get(data.id); - if (data.hasOwnProperty('err')) { - funcs.err(data.err); - this.outstandingSubscriptions.delete(data.id); - } - } else if (data.response === 'diff' && this.outstandingSubscriptions.has(data.id)) { - const funcs = this.outstandingSubscriptions.get(data.id); - funcs.event(data.json); - } else if (data.response === 'quit' && this.outstandingSubscriptions.has(data.id)) { - const funcs = this.outstandingSubscriptions.get(data.id); - funcs.quit(data); - this.outstandingSubscriptions.delete(data.id); - } else { - console.log('Unrecognized response', data); - } - // An incoming message, for example: - // { - // id: 10, - // json: { - // 'chat-update' : { // This is where we hook our "on" handlers like "onChatUpdate" - // message: { - // envelope: { - // author: 'zod', - // letter: { - // text: 'hi' - // }, - // number: 10, - // uid: 'saludhafhsdf', - // when: 124459 - // }, - // path: '/~zod/mailbox' - // } - // } - // } - // } - } - }); - this.sseClient!.addEventListener('error', function(event: Event) { - console.error('pipe error', event); - }); - - } - return this.sseClient; - } - - addEventListener(callback: (data: any) => void) { - return this.eventSource().addEventListener('message', (event: MessageEvent) => { - if (event.data && JSON.parse(event.data)) { - callback(JSON.parse(event.data)); - } - }); - } - - /** - * Autoincrements the next event ID for the appropriate channel. - */ - getEventId(): number { - this.lastEventId = Number(this.lastEventId) + 1; - return this.lastEventId; - } - - /** - * Acknowledges an event. - * - * @param eventId The event to acknowledge. - */ - ack(eventId: number): Promise { - return this.sendMessage('ack', { 'event-id': eventId }); - } - - /** - * This is a wrapper method that can be used to send any action with data. - * - * Every message sent has some common parameters, like method, headers, and data - * structure, so this method exists to prevent duplication. - * - * @param action The action to send - * @param data The data to send with the action - * - * @returns void | number If successful, returns the number of the message that was sent - */ - async sendMessage(action: Action, data?: object): Promise { - - const id = this.getEventId(); - if (this.verbose) { - console.log(`Sending message ${id}:`, action, data,); - } - let response: Response | undefined; - try { - response = await fetch(this.channelUrl, { - ...this.fetchOptions, - method: 'put', - body: JSON.stringify([{ - id, - action, - ...data, - }]), - }); - } catch (error) { - console.error('message error', error); - response = undefined; - } - if (this.verbose) { - console.log(`Received from message ${id}: `, response); - } - return id; - } - - /** - * Pokes a ship with data. - * - * @param app The app to poke - * @param mark The mark of the data being sent - * @param json The data to send - */ - poke(params: PokeInterface): Promise { - const { app, mark, json, onSuccess, onError } = {onSuccess: () => {}, onError: () => {}, ...params}; - return new Promise((resolve, reject) => { - this - .sendMessage('poke', { ship: this.ship, app, mark, json }) - .then(pokeId => { - if (!pokeId) { - return reject('Poke failed'); - } - if (!this.sseClient) resolve(pokeId); // A poke may occur before a listener has been opened - this.outstandingPokes.set(pokeId, { - onSuccess: () => { - onSuccess(); - resolve(pokeId); - }, - onError: (event) => { - onError(event); - reject(event.err); - } - }); - }).catch(error => { - console.error(error); - }); - }); - } - - /** - * Subscribes to a path on an app on a ship. - * - * @param app The app to subsribe to - * @param path The path to which to subscribe - * @param handlers Handlers to deal with various events of the subscription - */ - async subscribe(params: SubscriptionRequestInterface): Promise { - const { app, path, err, event, quit } = { err: () => {}, event: () => {}, quit: () => {}, ...params }; - - const subscriptionId = await this.sendMessage('subscribe', { ship: this.ship, app, path }); - console.log('subscribed', subscriptionId); - - if (!subscriptionId) return; - - this.outstandingSubscriptions.set(subscriptionId, { - err, event, quit - }); - - return subscriptionId; - } - - /** - * Unsubscribes to a given subscription. - * - * @param subscription - */ - unsubscribe(subscription: string): Promise { - return this.sendMessage('unsubscribe', { subscription }); - } - - /** - * Deletes the connection to a channel. - */ - delete(): Promise { - return this.sendMessage('delete'); - } - - /** - * - * @param app The app into which to scry - * @param path The path at which to scry - */ - async scry(app: string, path: string): Promise { - const response = await fetch(`/~/scry/${app}${path}.json`, this.fetchOptions); - return await response.json(); - } - - /** - * - * @param inputMark The mark of the data being sent - * @param outputMark The mark of the data being returned - * @param threadName The thread to run - * @param body The data to send to the thread - */ - async spider(params: Thread): Promise { - const { inputMark, outputMark, threadName, body } = params; - const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, { - ...this.fetchOptions, - method: 'POST', - body: JSON.stringify(body) - }); - - return res.json(); - } - - app(appName: string): UrbitApp { - const appClass = Urbit.apps.get(appName); - if (!appClass) { - throw new Error(`App ${appName} not found`); - } - return new appClass(this); - } - - /** - * Utility function to connect to a ship that has its *.arvo.network domain configured. - * - * @param name Name of the ship e.g. zod - * @param code Code to log in - */ - static async onArvoNetwork(ship: string, code: string): Promise { - const url = `https://${ship}.arvo.network`; - return await Urbit.authenticate({ ship, url, code }); - } - - static extend(appClass: any): void { - Urbit.apps.set(appClass.app, appClass); - } -} - - - -export default Urbit; +export * from './types'; +import Urbit from './Urbit'; +export { Urbit as default }; \ No newline at end of file diff --git a/pkg/npm/http-api/src/types.ts b/pkg/npm/http-api/src/types.ts new file mode 100644 index 0000000000..04cc62da5d --- /dev/null +++ b/pkg/npm/http-api/src/types.ts @@ -0,0 +1,66 @@ +import { Action, Poke, Scry, Thread } from '@urbit/api'; + +export interface PokeHandlers { + onSuccess?: () => void; + onError?: (e: any) => void; +} + +export type PokeInterface = PokeHandlers & Poke; + +export interface AuthenticationInterface { + ship: string; + url: string; + code: string; + verbose?: boolean; +} + +export interface SubscriptionInterface { + err?(error: any): void; + event?(data: any): void; + quit?(data: any): void; +} + +export type SubscriptionRequestInterface = SubscriptionInterface & { + app: string; + path: string; +} + +export interface headers { + 'Content-Type': string; + Cookie?: string; +} + +export interface UrbitInterface { + uid: string; + lastEventId: number; + lastAcknowledgedEventId: number; + sseClientInitialized: boolean; + cookie?: string | undefined; + outstandingPokes: Map; + outstandingSubscriptions: Map; + verbose?: boolean; + ship?: string | null; + connect(): void; + connect(): Promise; + eventSource(): void; + getEventId(): number; + ack(eventId: number): Promise; + sendMessage(action: Action, data?: object): Promise; + poke(params: PokeInterface): Promise; + subscribe(params: SubscriptionRequestInterface): Promise; + unsubscribe(subscription: string): Promise; + delete(): Promise; + scry(params: Scry): Promise; + thread(params: Thread): Promise; +} + +export interface CustomEventHandler { + (data: any, response: string): void; +} + +export interface SSEOptions { + headers?: { + Cookie?: string + }; + withCredentials?: boolean; +} diff --git a/pkg/npm/http-api/src/types/index.d.ts b/pkg/npm/http-api/src/types/index.d.ts deleted file mode 100644 index 57d095f769..0000000000 --- a/pkg/npm/http-api/src/types/index.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Action, Mark, Poke } from '../../../api/index'; - -export interface PokeHandlers { - onSuccess?: () => void; - onError?: (e: any) => void; -} - -export type PokeInterface = PokeHandlers & Poke; - - -export interface AuthenticationInterface { - ship: string; - url: string; - code: string; - verbose?: boolean; -} - -export interface SubscriptionInterface { - err?(error: any): void; - event?(data: any): void; - quit?(data: any): void; -} - -export type SubscriptionRequestInterface = SubscriptionInterface & { - app: string; - path: string; -} - -export interface headers { - 'Content-Type': string; - Cookie?: string; -} - -export interface UrbitInterface { - connect(): void; -} - -export interface CustomEventHandler { - (data: any, response: string): void; -} - -export interface SSEOptions { - headers?: { - Cookie?: string - }; - withCredentials?: boolean; -} diff --git a/pkg/npm/http-api/tsconfig-cjs.json b/pkg/npm/http-api/tsconfig-cjs.json deleted file mode 100644 index ec2ee62835..0000000000 --- a/pkg/npm/http-api/tsconfig-cjs.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "CommonJS", - "outDir": "./dist/cjs" - }, -} \ No newline at end of file diff --git a/pkg/npm/http-api/tsconfig.json b/pkg/npm/http-api/tsconfig.json index b5703e6ed0..2245cf9dc3 100644 --- a/pkg/npm/http-api/tsconfig.json +++ b/pkg/npm/http-api/tsconfig.json @@ -1,18 +1,19 @@ { - "include": ["src/**/*.ts"], + "include": ["src/*.ts"], "exclude": ["node_modules", "dist", "@types"], "compilerOptions": { - "outDir": "./dist/esm", - "module": "ES2020", + "outDir": "./dist", + "module": "ESNext", "noImplicitAny": true, - "target": "ES2020", + "target": "ESNext", "pretty": true, "moduleResolution": "node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "declaration": true, "sourceMap": true, - "strict": false - // "lib": ["ES2020"], + "strict": false, + "noErrorTruncation": true, + "allowJs": true, } } \ No newline at end of file diff --git a/pkg/npm/http-api/webpack.prod.js b/pkg/npm/http-api/webpack.prod.js deleted file mode 100644 index aab72b662f..0000000000 --- a/pkg/npm/http-api/webpack.prod.js +++ /dev/null @@ -1,109 +0,0 @@ -const path = require('path'); -const webpack = require('webpack'); - -const shared = { - mode: 'production', - entry: { - app: './src/index.ts' - }, - module: { - rules: [ - { - test: /\.(j|t)s$/, - use: { - loader: 'babel-loader', - options: { - presets: ['@babel/typescript'], - plugins: [ - '@babel/plugin-proposal-class-properties', - '@babel/plugin-proposal-object-rest-spread', - '@babel/plugin-proposal-optional-chaining', - ], - } - }, - exclude: /node_modules/ - } - ] - }, - resolve: { - extensions: ['.js', '.ts', '.ts'], - fallback: { - fs: false, - child_process: false, - util: require.resolve("util/"), - buffer: require.resolve('buffer/'), - assert: false, - http: require.resolve('stream-http'), - https: require.resolve('stream-http'), - stream: require.resolve('stream-browserify'), - zlib: require.resolve("browserify-zlib"), - } - }, - - optimization: { - minimize: false, - usedExports: true - } -}; - -const serverConfig = { - ...shared, - target: 'node', - output: { - filename: 'index.js', - path: path.resolve(__dirname, 'dist'), - library: 'Urbit', - libraryExport: 'default' - }, - plugins: [ - new webpack.ProvidePlugin({ - XMLHttpRequest: ['xmlhttprequest-ssl', 'XMLHttpRequest'], - EventSource: 'eventsource', - fetch: ['node-fetch', 'default'], - }), - ], -}; - -const browserConfig = { - ...shared, - target: 'web', - output: { - filename: 'browser.js', - path: path.resolve(__dirname, 'dist'), - library: 'Urbit', - libraryExport: 'default' - }, - plugins: [ - new webpack.ProvidePlugin({ - Buffer: 'buffer', - }), - ], -}; - - -const exampleBrowserConfig = { - ...shared, - mode: 'development', - entry: { - app: './src/example/browser.js' - }, - output: { - filename: 'browser.js', - path: path.resolve(__dirname, 'example'), - } -}; - -const exampleNodeConfig = { - ...shared, - mode: 'development', - target: 'node', - entry: { - app: './src/example/node.js' - }, - output: { - filename: 'node.js', - path: path.resolve(__dirname, 'example'), - } -}; - -module.exports = [ serverConfig, browserConfig, exampleBrowserConfig, exampleNodeConfig ]; \ No newline at end of file