From 2cba9b1a0b42d55075f9448fabdeca5e01b55c92 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Tue, 8 Jun 2021 14:25:43 +1000 Subject: [PATCH] @urbit/http-api: fix package configuration --- pkg/npm/http-api/Urbit.ts | 521 ----------------------------- pkg/npm/http-api/index.ts | 3 - pkg/npm/http-api/package-lock.json | 260 ++------------ pkg/npm/http-api/src/Urbit.ts | 296 ++++++++++++---- pkg/npm/http-api/src/types.ts | 18 +- pkg/npm/http-api/tsconfig.json | 4 +- pkg/npm/http-api/types.ts | 72 ---- pkg/npm/http-api/utils.ts | 82 ----- 8 files changed, 264 insertions(+), 992 deletions(-) delete mode 100644 pkg/npm/http-api/Urbit.ts delete mode 100644 pkg/npm/http-api/index.ts delete mode 100644 pkg/npm/http-api/types.ts delete mode 100644 pkg/npm/http-api/utils.ts diff --git a/pkg/npm/http-api/Urbit.ts b/pkg/npm/http-api/Urbit.ts deleted file mode 100644 index 4816469a5..000000000 --- a/pkg/npm/http-api/Urbit.ts +++ /dev/null @@ -1,521 +0,0 @@ -import { isBrowser, isNode } from 'browser-or-node'; -import { Action, Scry, Thread } from '@urbit/api'; -import { fetchEventSource, EventSourceMessage, EventStreamContentType } from '@microsoft/fetch-event-source'; - -import { AuthenticationInterface, SubscriptionInterface, CustomEventHandler, PokeInterface, SubscriptionRequestInterface, headers, UrbitInterface, SSEOptions, PokeHandlers, Message } 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; - - onError?: (error: any) => void = null; - - /** 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', - accept: '*', - 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 - ) { - if (isBrowser) { - window.addEventListener('beforeunload', this.delete); - } - 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; - } - if (this.lastEventId === 0) { - // Can't receive events until the channel is open - this.poke({ app: 'hood', mark: 'helm-hi', json: 'Opening API channel' }); - } - fetchEventSource(this.channelUrl, { - ...this.fetchOptions, - openWhenHidden: true, - onopen: async (response) => { - if (this.verbose) { - console.log('Opened eventsource', response); - } - if (response.ok && response.headers.get('content-type') === EventStreamContentType) { - return; // everything's good - } else if (response.status >= 400 && response.status < 500 && response.status !== 429) { - if (this.onError) { - this.onError(response.statusText); - } else { - throw new Error(); - } - } else { - if (this.onError) { - this.onError(response.statusText); - } else { - throw new Error(); - } - } - }, - onmessage: (event: EventSourceMessage) => { - if (this.verbose) { - console.log('Received SSE: ', event); - } - if (!event.id) return; - this.ack(Number(event.id)); - if (event.data && JSON.parse(event.data)) { - - const data: any = JSON.parse(event.data); - - if (data.response === 'diff') { - this.clearQueue(); - } - - 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) => { - if (this.onError) { - this.onError(error); - } else { - throw 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. - */ - async ack(eventId: number): Promise { - const message: Message = { - action: 'ack', - 'event-id': eventId - }; - await this.sendJSONtoChannel(message); - return 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,); - // } - // const message: Message = { id, action, ...data }; - // await this.sendJSONtoChannel(message); - // return id; - // } - - outstandingJSON: Message[] = []; - - debounceTimer: any = null; - debounceInterval = 500; - calm = true; - - sendJSONtoChannel(json: Message): Promise { - this.outstandingJSON.push(json); - return this.processQueue(); - } - - processQueue(): Promise { - return new Promise(async (resolve, reject) => { - const process = async () => { - if (this.calm) { - if (this.outstandingJSON.length === 0) resolve(true); - this.calm = false; // We are now occupied - const json = this.outstandingJSON; - const body = JSON.stringify(json); - this.outstandingJSON = []; - if (body === '[]') { - this.calm = true; - return resolve(false); - } - try { - await fetch(this.channelUrl, { - ...this.fetchOptions, - method: 'PUT', - body - }); - } catch (error) { - json.forEach(failed => this.outstandingJSON.push(failed)); - if (this.onError) { - this.onError(error); - } else { - throw error; - } - } - this.calm = true; - if (!this.sseClientInitialized) { - this.eventSource(); // We can open the channel for subscriptions once we've sent data over it - } - resolve(true); - } else { - clearTimeout(this.debounceTimer); - this.debounceTimer = setTimeout(process, this.debounceInterval); - resolve(false); - } - } - - this.debounceTimer = setTimeout(process, this.debounceInterval); - - - }); - } - - - - // resetDebounceTimer() { - // if (this.debounceTimer) { - // clearTimeout(this.debounceTimer); - // this.debounceTimer = null; - // } - // this.calm = false; - // this.debounceTimer = setTimeout(() => { - // this.calm = true; - // }, this.debounceInterval); - // } - - clearQueue() { - clearTimeout(this.debounceTimer); - this.debounceTimer = null; - } - - /** - * 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, - ship, - onSuccess, - onError - } = { - onSuccess: () => { }, - onError: () => { }, - ship: this.ship, - ...params - }; - return new Promise((resolve, reject) => { - const message: Message = { - id: this.getEventId(), - action: 'poke', - ship, - app, - mark, - json - }; - this.outstandingPokes.set(message.id, { - onSuccess: () => { - onSuccess(); - resolve(message.id); - }, - onError: (event) => { - onError(event); - reject(event.err); - } - }); - this.sendJSONtoChannel(message).then(() => { - resolve(message.id); - }); - }); - } - - /** - * 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, - ship, - err, - event, - quit - } = { - err: () => { }, - event: () => { }, - quit: () => { }, - ship: this.ship, - ...params - }; - - const message: Message = { - id: this.getEventId(), - action: 'subscribe', - ship, - app, - path - }; - - this.outstandingSubscriptions.set(message.id, { - app, path, err, event, quit - }); - - await this.sendJSONtoChannel(message); - - return message.id; - } - - /** - * Unsubscribes to a given subscription. - * - * @param subscription - */ - async unsubscribe(subscription: number) { - return this.sendJSONtoChannel({ - id: this.getEventId(), - action: 'unsubscribe', - subscription - }).then(() => { - this.outstandingSubscriptions.delete(subscription); - }); - } - - /** - * Deletes the connection to a channel. - */ - delete() { - if (isBrowser) { - navigator.sendBeacon(this.channelUrl, JSON.stringify([{ - action: 'delete' - }])); - } else { - // TODO - // 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(`${this.url}/~/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(`${this.url}/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/index.ts b/pkg/npm/http-api/index.ts deleted file mode 100644 index 8e242ef6b..000000000 --- a/pkg/npm/http-api/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './types'; -import Urbit from './Urbit'; -export { Urbit as default }; \ No newline at end of file diff --git a/pkg/npm/http-api/package-lock.json b/pkg/npm/http-api/package-lock.json index 85f2af07f..6a7b6bc16 100644 --- a/pkg/npm/http-api/package-lock.json +++ b/pkg/npm/http-api/package-lock.json @@ -1,6 +1,6 @@ { "name": "@urbit/http-api", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -479,6 +479,11 @@ "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, + "@types/lodash": { + "version": "4.14.170", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz", + "integrity": "sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==" + }, "@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", @@ -690,7 +695,9 @@ } }, "@urbit/api": { - "version": "file:../api", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@urbit/api/-/api-1.1.0.tgz", + "integrity": "sha512-oflDguSpB6u9VmlmbX23Z+aSjMCFj7QbmQEG6YjhZvuz2ok711/F5+B4NvGKOl0NfdG2ao2wK7m+z4AyH4pZjQ==", "requires": { "@babel/runtime": "^7.12.5", "@types/lodash": "^4.14.168", @@ -698,239 +705,13 @@ "big-integer": "^1.6.48", "immer": "^9.0.1", "lodash": "^4.17.20" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz", - "integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@blakeembrey/deque": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@blakeembrey/deque/-/deque-1.0.5.tgz", - "integrity": "sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==" - }, - "@blakeembrey/template": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@blakeembrey/template/-/template-1.0.0.tgz", - "integrity": "sha512-J6WGZqCLdRMHUkyRG6fBSIFJ0rL60/nsQNh5rQvsYZ5u0PsKw6XQcJcA3DWvd9cN3j/IQx5yB1fexhCafwwUUw==" - }, - "@types/lodash": { - "version": "4.14.169", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.169.tgz", - "integrity": "sha512-DvmZHoHTFJ8zhVYwCLWbQ7uAbYQEk52Ev2/ZiQ7Y7gQGeV9pjBqjnQpECMHfKS1rCYAhMI7LHVxwyZLZinJgdw==" - }, - "@urbit/eslint-config": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@urbit/eslint-config/-/eslint-config-1.0.0.tgz", - "integrity": "sha512-Xmzb6MvM7KorlPJEq/hURZZ4BHSVy/7CoQXWogsBSTv5MOZnMqwNKw6yt24k2AO/2UpHwjGptimaNLqFfesJbw==" - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" - }, - "big-integer": { - "version": "1.6.48", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", - "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.3.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" - }, - "immer": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.2.tgz", - "integrity": "sha512-mkcmzLtIfSp40vAqteRr1MbWNSoI7JE+/PB36FNPoSfJ9RQRmNKuTYCjKkyXyuq3Dgn07HuJBrwJd4ZSk2yUbw==" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "onchange": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/onchange/-/onchange-7.1.0.tgz", - "integrity": "sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==", - "requires": { - "@blakeembrey/deque": "^1.0.5", - "@blakeembrey/template": "^1.0.0", - "arg": "^4.1.3", - "chokidar": "^3.3.1", - "cross-spawn": "^7.0.1", - "ignore": "^5.1.4", - "tree-kill": "^1.2.2" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "picomatch": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", - "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==" - }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - } } }, + "@urbit/eslint-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@urbit/eslint-config/-/eslint-config-1.0.0.tgz", + "integrity": "sha512-Xmzb6MvM7KorlPJEq/hURZZ4BHSVy/7CoQXWogsBSTv5MOZnMqwNKw6yt24k2AO/2UpHwjGptimaNLqFfesJbw==" + }, "@webassemblyjs/ast": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz", @@ -1346,6 +1127,11 @@ "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", "dev": true }, + "big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -3250,6 +3036,11 @@ "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true }, + "immer": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.2.tgz", + "integrity": "sha512-mkcmzLtIfSp40vAqteRr1MbWNSoI7JE+/PB36FNPoSfJ9RQRmNKuTYCjKkyXyuq3Dgn07HuJBrwJd4ZSk2yUbw==" + }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -3753,8 +3544,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "loglevel": { "version": "1.7.1", diff --git a/pkg/npm/http-api/src/Urbit.ts b/pkg/npm/http-api/src/Urbit.ts index 4f0136168..18a1141f3 100644 --- a/pkg/npm/http-api/src/Urbit.ts +++ b/pkg/npm/http-api/src/Urbit.ts @@ -1,8 +1,8 @@ import { isBrowser, isNode } from 'browser-or-node'; import { Action, Scry, Thread } from '@urbit/api'; -import { fetchEventSource, EventSourceMessage } from '@microsoft/fetch-event-source'; +import { fetchEventSource, EventSourceMessage, EventStreamContentType } from '@microsoft/fetch-event-source'; -import { AuthenticationInterface, SubscriptionInterface, CustomEventHandler, PokeInterface, SubscriptionRequestInterface, headers, UrbitInterface, SSEOptions, PokeHandlers } from './types'; +import { AuthenticationInterface, SubscriptionInterface, CustomEventHandler, PokeInterface, SubscriptionRequestInterface, headers, UrbitInterface, SSEOptions, PokeHandlers, Message } from './types'; import { uncamelize, hexString } from './utils'; /** @@ -52,7 +52,7 @@ export class Urbit implements UrbitInterface { * disconnect function may be called exactly once. */ - outstandingSubscriptions: Map = new Map(); + outstandingSubscriptions: Map = new Map(); /** * Ship can be set, in which case we can do some magic stuff like send chats @@ -64,6 +64,8 @@ export class Urbit implements UrbitInterface { */ verbose?: boolean; + onError?: (error: any) => void = null; + /** This is basic interpolation to get the channel URL of an instantiated Urbit connection. */ get channelUrl(): string { return `${this.url}/~/channel/${this.uid}`; @@ -78,6 +80,7 @@ export class Urbit implements UrbitInterface { } return { credentials: 'include', + accept: '*', headers }; } @@ -90,8 +93,11 @@ export class Urbit implements UrbitInterface { */ constructor( public url: string, - public code: string + public code?: string ) { + if (isBrowser) { + window.addEventListener('beforeunload', this.delete); + } return this; } @@ -144,7 +150,8 @@ export class Urbit implements UrbitInterface { /** * Initializes the SSE pipe for the appropriate channel. */ - eventSource(): void{ + eventSource(): Promise { + return new Promise((resolve, reject) => { if (!this.sseClientInitialized) { const sseOptions: SSEOptions = { headers: {} @@ -154,15 +161,50 @@ export class Urbit implements UrbitInterface { } else if (isNode) { sseOptions.headers.Cookie = this.cookie; } + if (this.lastEventId === 0) { + // Can't receive events until the channel is open + this.poke({ app: 'hood', mark: 'helm-hi', json: 'Opening API channel' }); + } fetchEventSource(this.channelUrl, { - // withCredentials: true, + ...this.fetchOptions, + openWhenHidden: true, + onopen: async (response) => { + if (this.verbose) { + console.log('Opened eventsource', response); + } + if (response.ok) { + resolve(); + return; // everything's good + } else if (response.status >= 400 && response.status < 500 && response.status !== 429) { + reject(); + if (this.onError) { + this.onError(response.statusText); + } else { + throw new Error(); + } + } else { + reject(); + if (this.onError) { + this.onError(response.statusText); + } else { + throw new Error(); + } + } + }, onmessage: (event: EventSourceMessage) => { if (this.verbose) { console.log('Received SSE: ', event); } + if (!event.id) return; this.ack(Number(event.id)); if (event.data && JSON.parse(event.data)) { + const data: any = JSON.parse(event.data); + + if (data.response === 'diff') { + this.clearQueue(); + } + if (data.response === 'poke' && this.outstandingPokes.has(data.id)) { const funcs = this.outstandingPokes.get(data.id); if (data.hasOwnProperty('ok')) { @@ -193,12 +235,17 @@ export class Urbit implements UrbitInterface { } }, onerror: (error) => { - console.error('pipe error', error); + if (this.onError) { + this.onError(error); + } else { + throw error; + } } }); this.sseClientInitialized = true; } - return; + resolve(); + }); } /** @@ -214,8 +261,13 @@ export class Urbit implements UrbitInterface { * * @param eventId The event to acknowledge. */ - ack(eventId: number): Promise { - return this.sendMessage('ack', { 'event-id': eventId }); + async ack(eventId: number): Promise { + const message: Message = { + action: 'ack', + 'event-id': eventId + }; + await this.sendJSONtoChannel(message); + return eventId; } /** @@ -229,31 +281,88 @@ export class Urbit implements UrbitInterface { * * @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; + // async sendMessage(action: Action, data?: object): Promise { + // const id = this.getEventId(); + // if (this.verbose) { + // console.log(`Sending message ${id}:`, action, data,); + // } + // const message: Message = { id, action, ...data }; + // await this.sendJSONtoChannel(message); + // return id; + // } + + outstandingJSON: Message[] = []; + + debounceTimer: any = null; + debounceInterval = 500; + calm = true; + + sendJSONtoChannel(json: Message): Promise { + this.outstandingJSON.push(json); + return this.processQueue(); + } + + processQueue(): Promise { + return new Promise(async (resolve, reject) => { + const process = async () => { + if (this.calm) { + if (this.outstandingJSON.length === 0) resolve(true); + this.calm = false; // We are now occupied + const json = this.outstandingJSON; + const body = JSON.stringify(json); + this.outstandingJSON = []; + if (body === '[]') { + this.calm = true; + return resolve(false); + } + try { + await fetch(this.channelUrl, { + ...this.fetchOptions, + method: 'PUT', + body + }); + } catch (error) { + json.forEach(failed => this.outstandingJSON.push(failed)); + if (this.onError) { + this.onError(error); + } else { + throw error; + } + } + this.calm = true; + if (!this.sseClientInitialized) { + this.eventSource(); // We can open the channel for subscriptions once we've sent data over it + } + resolve(true); + } else { + clearTimeout(this.debounceTimer); + this.debounceTimer = setTimeout(process, this.debounceInterval); + resolve(false); + } + } + + this.debounceTimer = setTimeout(process, this.debounceInterval); + + + }); + } + + + + // resetDebounceTimer() { + // if (this.debounceTimer) { + // clearTimeout(this.debounceTimer); + // this.debounceTimer = null; + // } + // this.calm = false; + // this.debounceTimer = setTimeout(() => { + // this.calm = true; + // }, this.debounceInterval); + // } + + clearQueue() { + clearTimeout(this.debounceTimer); + this.debounceTimer = null; } /** @@ -263,29 +372,42 @@ export class Urbit implements UrbitInterface { * @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 }; + poke(params: PokeInterface): Promise { + const { + app, + mark, + json, + ship, + onSuccess, + onError + } = { + onSuccess: () => { }, + onError: () => { }, + ship: this.ship, + ...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); - }); + const message: Message = { + id: this.getEventId(), + action: 'poke', + ship, + app, + mark, + json + }; + this.outstandingPokes.set(message.id, { + onSuccess: () => { + onSuccess(); + resolve(message.id); + }, + onError: (event) => { + onError(event); + reject(event.err); + } + }); + this.sendJSONtoChannel(message).then(() => { + resolve(message.id); + }); }); } @@ -296,18 +418,37 @@ export class Urbit implements UrbitInterface { * @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 }; + async subscribe(params: SubscriptionRequestInterface): Promise { + const { + app, + path, + ship, + err, + event, + quit + } = { + err: () => { }, + event: () => { }, + quit: () => { }, + ship: this.ship, + ...params + }; - const subscriptionId = await this.sendMessage('subscribe', { ship: this.ship, app, path }); + const message: Message = { + id: this.getEventId(), + action: 'subscribe', + ship, + app, + path + }; - if (!subscriptionId) return; - - this.outstandingSubscriptions.set(subscriptionId, { - err, event, quit + this.outstandingSubscriptions.set(message.id, { + app, path, err, event, quit }); - return subscriptionId; + await this.sendJSONtoChannel(message); + + return message.id; } /** @@ -315,15 +456,28 @@ export class Urbit implements UrbitInterface { * * @param subscription */ - unsubscribe(subscription: string): Promise { - return this.sendMessage('unsubscribe', { subscription }); + async unsubscribe(subscription: number) { + return this.sendJSONtoChannel({ + id: this.getEventId(), + action: 'unsubscribe', + subscription + }).then(() => { + this.outstandingSubscriptions.delete(subscription); + }); } /** * Deletes the connection to a channel. */ - delete(): Promise { - return this.sendMessage('delete'); + delete() { + if (isBrowser) { + navigator.sendBeacon(this.channelUrl, JSON.stringify([{ + action: 'delete' + }])); + } else { + // TODO + // this.sendMessage('delete'); + } } /** @@ -333,7 +487,7 @@ export class Urbit implements UrbitInterface { */ async scry(params: Scry): Promise { const { app, path } = params; - const response = await fetch(`/~/scry/${app}${path}.json`, this.fetchOptions); + const response = await fetch(`${this.url}/~/scry/${app}${path}.json`, this.fetchOptions); return await response.json(); } @@ -346,7 +500,7 @@ export class Urbit implements UrbitInterface { */ async thread(params: Thread): Promise { const { inputMark, outputMark, threadName, body } = params; - const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, { + const res = await fetch(`${this.url}/spider/${inputMark}/${threadName}/${outputMark}.json`, { ...this.fetchOptions, method: 'POST', body: JSON.stringify(body) diff --git a/pkg/npm/http-api/src/types.ts b/pkg/npm/http-api/src/types.ts index 04cc62da5..d544cae3c 100644 --- a/pkg/npm/http-api/src/types.ts +++ b/pkg/npm/http-api/src/types.ts @@ -37,19 +37,20 @@ export interface UrbitInterface { sseClientInitialized: boolean; cookie?: string | undefined; outstandingPokes: Map; - outstandingSubscriptions: Map; + outstandingSubscriptions: Map; verbose?: boolean; ship?: string | null; + onError?: (error: any) => void; 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; + // sendMessage(action: Action, data?: object): Promise; + poke(params: PokeInterface): Promise; + subscribe(params: SubscriptionRequestInterface): Promise; + unsubscribe(subscription: number): Promise; + delete(): void; scry(params: Scry): Promise; thread(params: Thread): Promise; } @@ -64,3 +65,8 @@ export interface SSEOptions { }; withCredentials?: boolean; } + +export interface Message extends Record { + action: Action; + id?: number; +} \ No newline at end of file diff --git a/pkg/npm/http-api/tsconfig.json b/pkg/npm/http-api/tsconfig.json index 7e7396492..69acc414e 100644 --- a/pkg/npm/http-api/tsconfig.json +++ b/pkg/npm/http-api/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["*.ts"], + "include": ["src/**/*"], "exclude": ["node_modules", "dist", "@types"], "compilerOptions": { "outDir": "./dist", @@ -20,4 +20,4 @@ "*" : ["./node_modules/@types/*", "*"] } } -} \ No newline at end of file +} diff --git a/pkg/npm/http-api/types.ts b/pkg/npm/http-api/types.ts deleted file mode 100644 index d544cae3c..000000000 --- a/pkg/npm/http-api/types.ts +++ /dev/null @@ -1,72 +0,0 @@ -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; - onError?: (error: any) => void; - 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: number): Promise; - delete(): void; - scry(params: Scry): Promise; - thread(params: Thread): Promise; -} - -export interface CustomEventHandler { - (data: any, response: string): void; -} - -export interface SSEOptions { - headers?: { - Cookie?: string - }; - withCredentials?: boolean; -} - -export interface Message extends Record { - action: Action; - id?: number; -} \ No newline at end of file diff --git a/pkg/npm/http-api/utils.ts b/pkg/npm/http-api/utils.ts deleted file mode 100644 index 94c0998cb..000000000 --- a/pkg/npm/http-api/utils.ts +++ /dev/null @@ -1,82 +0,0 @@ -import * as http from 'http'; - -interface HttpResponse { - req: http.ClientRequest; - res: http.IncomingMessage; - data: string; -} - -export function request( - url: string, - options: http.ClientRequestArgs, - body?: string -): Promise { - return new Promise((resolve, reject) => { - const req = http.request(url, options, res => { - let data = ""; - res.on("data", chunk => { - data += chunk; - }); - res.on("end", () => { - resolve({ req, res, data }); - }); - res.on("error", e => { - reject(e); - }); - }); - if (body) { - req.write(body); - } - req.end(); - }); -} - -export function camelize(str: string) { - return str - .replace(/\s(.)/g, function($1: string) { return $1.toUpperCase(); }) - .replace(/\s/g, '') - .replace(/^(.)/, function($1: string) { return $1.toLowerCase(); }); -} - -export function uncamelize(str: string, separator = '-') { - // Replace all capital letters by separator followed by lowercase one - var str = str.replace(/[A-Z]/g, function (letter: string) { - return separator + letter.toLowerCase(); - }); - return str.replace(new RegExp('^' + separator), ''); -} - -/** - * Returns a hex string of given length. - * - * Poached from StackOverflow. - * - * @param len Length of hex string to return. - */ -export function hexString(len: number): string { - const maxlen = 8; - const min = Math.pow(16, Math.min(len, maxlen) - 1); - const max = Math.pow(16, Math.min(len, maxlen)) - 1; - const n = Math.floor(Math.random() * (max - min + 1)) + min; - let r = n.toString(16); - while (r.length < len) { - r = r + hexString(len - maxlen); - } - return r; -} - -/** - * Generates a random UID. - * - * Copied from https://github.com/urbit/urbit/blob/137e4428f617c13f28ed31e520eff98d251ed3e9/pkg/interface/src/lib/util.js#L3 - */ -export function uid(): string { - let str = '0v'; - str += Math.ceil(Math.random() * 8) + '.'; - for (let i = 0; i < 5; i++) { - let _str = Math.ceil(Math.random() * 10000000).toString(32); - _str = ('00000' + _str).substr(-5, 5); - str += _str + '.'; - } - return str.slice(0, -1); -} \ No newline at end of file