mirror of
https://github.com/urbit/shrub.git
synced 2024-12-21 01:41:37 +03:00
http-api: changed eventsource tool
This commit is contained in:
parent
cc6adb3ffd
commit
ece8836a7e
@ -1,17 +1,32 @@
|
|||||||
/*
|
/*
|
||||||
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
|
* 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.
|
* 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/)
|
* 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".
|
* 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/).
|
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||||
*/
|
*/
|
||||||
/******/ (() => { // webpackBootstrap
|
/******/ (() => { // webpackBootstrap
|
||||||
|
/******/ var __webpack_modules__ = ({
|
||||||
|
|
||||||
|
/***/ "./src/example/browser.js":
|
||||||
/*!********************************!*\
|
/*!********************************!*\
|
||||||
!*** ./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?");
|
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"]();
|
||||||
|
/******/
|
||||||
/******/ })()
|
/******/ })()
|
||||||
;
|
;
|
@ -1,17 +1,32 @@
|
|||||||
/*
|
/*
|
||||||
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
|
* 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.
|
* 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/)
|
* 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".
|
* 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/).
|
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||||
*/
|
*/
|
||||||
/******/ (() => { // webpackBootstrap
|
/******/ (() => { // webpackBootstrap
|
||||||
|
/******/ var __webpack_modules__ = ({
|
||||||
|
|
||||||
|
/***/ "./src/example/node.js":
|
||||||
/*!*****************************!*\
|
/*!*****************************!*\
|
||||||
!*** ./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?");
|
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"]();
|
||||||
|
/******/
|
||||||
/******/ })()
|
/******/ })()
|
||||||
;
|
;
|
@ -1,2 +0,0 @@
|
|||||||
import Urbit from './dist';
|
|
||||||
export { Urbit as default, Urbit };
|
|
@ -8,20 +8,15 @@
|
|||||||
"url": "ssh://git@github.com/urbit/urbit.git",
|
"url": "ssh://git@github.com/urbit/urbit.git",
|
||||||
"directory": "pkg/npm/http-api"
|
"directory": "pkg/npm/http-api"
|
||||||
},
|
},
|
||||||
"main": "dist/cjs/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/esm/index.js",
|
"types": "dist/index.d.ts",
|
||||||
"browser": "dist/esm/index.js",
|
|
||||||
"types": "dist/esm/index.d.ts",
|
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"src"
|
"src"
|
||||||
],
|
],
|
||||||
"engines": {
|
|
||||||
"node": ">=13"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"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/*"
|
"clean": "rm -rf dist/*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {},
|
"peerDependencies": {},
|
||||||
@ -38,31 +33,29 @@
|
|||||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
|
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
|
||||||
"@babel/preset-typescript": "^7.12.1",
|
"@babel/preset-typescript": "^7.12.1",
|
||||||
|
"@types/browser-or-node": "^1.2.0",
|
||||||
"@types/eventsource": "^1.1.5",
|
"@types/eventsource": "^1.1.5",
|
||||||
"@types/react": "^16.9.56",
|
"@types/react": "^16.9.56",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.7.0",
|
"@typescript-eslint/eslint-plugin": "^4.7.0",
|
||||||
"@typescript-eslint/parser": "^4.7.0",
|
"@typescript-eslint/parser": "^4.7.0",
|
||||||
"@types/browser-or-node": "^1.2.0",
|
|
||||||
"babel-loader": "^8.2.1",
|
"babel-loader": "^8.2.1",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"tslib": "^2.0.3",
|
"tslib": "^2.0.3",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^3.9.7",
|
||||||
|
"util": "^0.12.3",
|
||||||
"webpack": "^5.4.0",
|
"webpack": "^5.4.0",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-dev-server": "^3.11.0"
|
"webpack-dev-server": "^3.11.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@microsoft/fetch-event-source": "^2.0.0",
|
||||||
|
"@urbit/api": "file:../api",
|
||||||
"browser-or-node": "^1.3.0",
|
"browser-or-node": "^1.3.0",
|
||||||
"browserify-zlib": "^0.2.0",
|
"browserify-zlib": "^0.2.0",
|
||||||
"buffer": "^5.7.1",
|
"buffer": "^6.0.3",
|
||||||
"encoding": "^0.1.13",
|
|
||||||
"eventsource": "^1.0.7",
|
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"stream-http": "^3.1.1",
|
"stream-http": "^3.1.1"
|
||||||
"util": "^0.12.3",
|
|
||||||
"xmlhttprequest": "^1.8.0",
|
|
||||||
"xmlhttprequest-ssl": "^1.6.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
372
pkg/npm/http-api/src/Urbit.ts
Normal file
372
pkg/npm/http-api/src/Urbit.ts
Normal file
@ -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<number, PokeHandlers> = 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<number, SubscriptionInterface> = 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<void> {
|
||||||
|
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<void | number> {
|
||||||
|
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<void | number> {
|
||||||
|
|
||||||
|
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<T>(params: PokeInterface<T>): Promise<void | number> {
|
||||||
|
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<void | number> {
|
||||||
|
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<void | number> {
|
||||||
|
return this.sendMessage('unsubscribe', { subscription });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the connection to a channel.
|
||||||
|
*/
|
||||||
|
delete(): Promise<void | number> {
|
||||||
|
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<void | any> {
|
||||||
|
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<T>(params: Thread<T>): Promise<T> {
|
||||||
|
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<Urbit> {
|
||||||
|
const url = `https://${ship}.arvo.network`;
|
||||||
|
return await Urbit.authenticate({ ship, url, code });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default Urbit;
|
@ -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
|
|
||||||
}
|
|
@ -1,457 +1,3 @@
|
|||||||
import { isBrowser, isNode } from 'browser-or-node';
|
export * from './types';
|
||||||
import { Action, Thread } from '../../api';
|
import Urbit from './Urbit';
|
||||||
|
export { Urbit as default };
|
||||||
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<number, PokeHandlers> = 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<number, SubscriptionInterface> = 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<string, typeof UrbitApp> = 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<void> {
|
|
||||||
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<void | number> {
|
|
||||||
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<void | number> {
|
|
||||||
|
|
||||||
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<T>(params: PokeInterface<T>): Promise<void | number> {
|
|
||||||
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<void | number> {
|
|
||||||
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<void | number> {
|
|
||||||
return this.sendMessage('unsubscribe', { subscription });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the connection to a channel.
|
|
||||||
*/
|
|
||||||
delete(): Promise<void | number> {
|
|
||||||
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<void | any> {
|
|
||||||
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<T>(params: Thread<T>): Promise<T> {
|
|
||||||
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<Urbit> {
|
|
||||||
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;
|
|
66
pkg/npm/http-api/src/types.ts
Normal file
66
pkg/npm/http-api/src/types.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Action, Poke, Scry, Thread } from '@urbit/api';
|
||||||
|
|
||||||
|
export interface PokeHandlers {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onError?: (e: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PokeInterface<T> = PokeHandlers & Poke<T>;
|
||||||
|
|
||||||
|
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<number, PokeHandlers>;
|
||||||
|
outstandingSubscriptions: Map<number, SubscriptionInterface>;
|
||||||
|
verbose?: boolean;
|
||||||
|
ship?: string | null;
|
||||||
|
connect(): void;
|
||||||
|
connect(): Promise<void>;
|
||||||
|
eventSource(): void;
|
||||||
|
getEventId(): number;
|
||||||
|
ack(eventId: number): Promise<void | number>;
|
||||||
|
sendMessage(action: Action, data?: object): Promise<void | number>;
|
||||||
|
poke<T>(params: PokeInterface<T>): Promise<void | number>;
|
||||||
|
subscribe(params: SubscriptionRequestInterface): Promise<void | number>;
|
||||||
|
unsubscribe(subscription: string): Promise<void | number>;
|
||||||
|
delete(): Promise<void | number>;
|
||||||
|
scry(params: Scry): Promise<void | any>;
|
||||||
|
thread<T>(params: Thread<T>): Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomEventHandler {
|
||||||
|
(data: any, response: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SSEOptions {
|
||||||
|
headers?: {
|
||||||
|
Cookie?: string
|
||||||
|
};
|
||||||
|
withCredentials?: boolean;
|
||||||
|
}
|
47
pkg/npm/http-api/src/types/index.d.ts
vendored
47
pkg/npm/http-api/src/types/index.d.ts
vendored
@ -1,47 +0,0 @@
|
|||||||
import { Action, Mark, Poke } from '../../../api/index';
|
|
||||||
|
|
||||||
export interface PokeHandlers {
|
|
||||||
onSuccess?: () => void;
|
|
||||||
onError?: (e: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PokeInterface<T> = PokeHandlers & Poke<T>;
|
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "CommonJS",
|
|
||||||
"outDir": "./dist/cjs"
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/*.ts"],
|
||||||
"exclude": ["node_modules", "dist", "@types"],
|
"exclude": ["node_modules", "dist", "@types"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist/esm",
|
"outDir": "./dist",
|
||||||
"module": "ES2020",
|
"module": "ESNext",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"target": "ES2020",
|
"target": "ESNext",
|
||||||
"pretty": true,
|
"pretty": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": false
|
"strict": false,
|
||||||
// "lib": ["ES2020"],
|
"noErrorTruncation": true,
|
||||||
|
"allowJs": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 ];
|
|
Loading…
Reference in New Issue
Block a user