From b7b6f3d2a77f023bc57b8ff28316441bd5fe94eb Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 11 Jun 2018 22:19:26 -0500 Subject: [PATCH 1/5] Add experimental.datPeers API --- app/background-process/browser.js | 2 + .../networks/dat/extensions.js | 179 ++++++++++++++++++ .../networks/dat/library.js | 20 +- app/background-process/web-apis.js | 3 + .../web-apis/experimental/dat-peers.js | 77 ++++++++ .../external/experimental/dat-peers.js | 9 + app/lib/perms.js | 11 +- app/lib/web-apis/event-target.js | 7 +- app/lib/web-apis/experimental.js | 33 ++++ app/package.json | 2 + 10 files changed, 336 insertions(+), 7 deletions(-) create mode 100644 app/background-process/networks/dat/extensions.js create mode 100644 app/background-process/web-apis/experimental/dat-peers.js create mode 100644 app/lib/api-manifests/external/experimental/dat-peers.js diff --git a/app/background-process/browser.js b/app/background-process/browser.js index 4eaa9f017b..a893231878 100644 --- a/app/background-process/browser.js +++ b/app/background-process/browser.js @@ -52,9 +52,11 @@ var userSetupStatusLookupPromise var browserEvents = new EventEmitter() process.on('unhandledRejection', (reason, p) => { + console.error('Unhandled Rejection at: Promise', p, 'reason:', reason) debug('Unhandled Rejection at: Promise', p, 'reason:', reason) }) process.on('uncaughtException', (err) => { + console.error('Uncaught exception:', err) debug('Uncaught exception:', err) }) diff --git a/app/background-process/networks/dat/extensions.js b/app/background-process/networks/dat/extensions.js new file mode 100644 index 0000000000..5c8121d83a --- /dev/null +++ b/app/background-process/networks/dat/extensions.js @@ -0,0 +1,179 @@ +import EventEmitter from 'events' +import emitStream from 'emit-stream' +import {DatSessionDataExtMsg} from '@beaker/dat-session-data-ext-msg' +import {DatEphemeralExtMsg} from '@beaker/dat-ephemeral-ext-msg' + +// globals +// = + +var datSessionDataExtMsg = new DatSessionDataExtMsg() +var datEphemeralExtMsg = new DatEphemeralExtMsg() + +// exported api +// = + +export function setup () { + datEphemeralExtMsg.on('message', onEphemeralMsg) + datSessionDataExtMsg.on('session-data', onSessionDataMsg) +} + +// call this on every archive created in the library +export function attach (archive) { + datEphemeralExtMsg.watchDat(archive) + datSessionDataExtMsg.watchDat(archive) + archive._datPeersEvents = new EventEmitter() + archive._datPeersOnPeerAdd = (peer) => onPeerAdd(archive, peer) + archive._datPeersOnPeerRemove = (peer) => onPeerRemove(archive, peer) + archive.metadata.on('peer-add', archive._datPeersOnPeerAdd) + archive.metadata.on('peer-remove', archive._datPeersOnPeerRemove) +} + +// call this on every archive destroyed in the library +export function detach (archive) { + datEphemeralExtMsg.unwatchDat(archive) + datSessionDataExtMsg.unwatchDat(archive) + delete archive._datPeersEvents + archive.metadata.removeListener('peer-add', archive._datPeersOnPeerAdd) + archive.metadata.removeListener('peer-remove', archive._datPeersOnPeerRemove) +} + +// impl for datPeers.list() +export function listPeers (archive) { + return archive.metadata.peers.map(internalPeerObj => createWebAPIPeerObj(archive, internalPeerObj)) +} + +// impl for datPeers.getPeer(peerId) +export function getPeer (archive, peerId) { + var internalPeerObj = archive.metadata.peers.find(internalPeerObj => getPeerId(internalPeerObj) === peerId) + return createWebAPIPeerObj(archive, internalPeerObj) +} + +// impl for datPeers.broadcast(msg) +export function broadcastEphemeralMessage (archive, payload) { + datEphemeralExtMsg.broadcast(archive, encodeEphemeralMsg(payload)) +} + +// impl for datPeers.send(peerId, msg) +export function sendEphemeralMessage (archive, peerId, payload) { + datEphemeralExtMsg.send(archive, peerId, encodeEphemeralMsg(payload)) +} + +// impl for datPeers.getSessionData() +export function getSessionData (archive) { + return decodeSessionData(datSessionDataExtMsg.getLocalSessionData(archive)) +} + +// impl for datPeers.getSessionData(data) +export function setSessionData (archive, sessionData) { + datSessionDataExtMsg.setLocalSessionData(archive, encodeSessionData(sessionData)) +} + +export function createDatPeersStream (archive) { + return emitStream(archive._datPeersEvents) +} + +// events +// = + +function onPeerAdd (archive, internalPeerObj) { + if (getPeerId(internalPeerObj)) onHandshook() + else internalPeerObj.stream.stream.on('handshake', onHandshook) + + function onHandshook () { + var peerId = getPeerId(internalPeerObj) + + // send session data + if (datSessionDataExtMsg.getLocalSessionData(archive)) { + datSessionDataExtMsg.sendLocalSessionData(archive, peerId) + } + + // emit event + archive._datPeersEvents.emit('connect', { + peerId, + sessionData: getPeerSessionData(archive, peerId) + }) + } +} + +function onPeerRemove (archive, internalPeerObj) { + var peerId = getPeerId(internalPeerObj) + if (peerId) { + archive._datPeersEvents.emit('disconnect', { + peerId, + sessionData: getPeerSessionData(archive, peerId) + }) + } +} + +function onEphemeralMsg (archive, internalPeerObj, msg) { + var peerId = getPeerId(internalPeerObj) + archive._datPeersEvents.emit('message', { + peerId, + sessionData: getPeerSessionData(archive, peerId), + data: decodeEphemeralMsg(msg) + }) +} + +function onSessionDataMsg (archive, internalPeerObj, sessionData) { + archive._datPeersEvents.emit('session-data', { + peerId: getPeerId(internalPeerObj), + sessionData: decodeSessionData(sessionData) + }) +} + +// internal methods +// = + +function getPeerId (internalPeerObj) { + var feedStream = internalPeerObj.stream + var protocolStream = feedStream.stream + return protocolStream.remoteId ? protocolStream.remoteId.toString('hex') : null +} + +function getPeerSessionData (archive, peerId) { + return decodeSessionData(datSessionDataExtMsg.getSessionData(archive, peerId)) +} + +function createWebAPIPeerObj (archive, internalPeerObj) { + var id = getPeerId(internalPeerObj) + var sessionData = getPeerSessionData(archive, id) + return {id, sessionData} +} + +function encodeEphemeralMsg (payload) { + var contentType + if (Buffer.isBuffer(payload)) { + contentType = 'application/octet-stream' + } else { + contentType = 'application/json' + payload = Buffer.from(JSON.stringify(payload), 'utf8') + } + return {contentType, payload} +} + +function decodeEphemeralMsg (msg) { + var payload + if (msg.contentType === 'application/json') { + try { + payload = JSON.parse(msg.payload.toString('utf8')) + } catch (e) { + console.error('Failed to parse ephemeral message', e, msg) + payload = null + } + } + return payload +} + +function encodeSessionData (obj) { + return Buffer.from(JSON.stringify(obj), 'utf8') +} + +function decodeSessionData (sessionData) { + if (!sessionData || sessionData.length === 0) return null + try { + return JSON.parse(sessionData.toString('utf8')) + } catch (e) { + console.error('Failed to parse local session data', e, sessionData) + return null + } +} diff --git a/app/background-process/networks/dat/library.js b/app/background-process/networks/dat/library.js index 6520aa194b..c699661417 100644 --- a/app/background-process/networks/dat/library.js +++ b/app/background-process/networks/dat/library.js @@ -20,9 +20,11 @@ import * as archivesDb from '../../dbs/archives' import * as datGC from './garbage-collector' import * as folderSync from './folder-sync' import {addArchiveSwarmLogging} from './logging-utils' +import * as datExtensions from './extensions' import hypercoreProtocol from 'hypercore-protocol' import hyperdrive from 'hyperdrive' + // network modules import swarmDefaults from 'datland-swarm-defaults' import discoverySwarm from 'discovery-swarm' @@ -105,6 +107,9 @@ export function setup ({logfilePath}) { }) }) + // setup extensions + datExtensions.setup() + // setup the archive swarm datGC.setup() archiveSwarm = discoverySwarm(swarmDefaults({ @@ -130,6 +135,10 @@ export function createEventStream () { return emitStream(archivesEvents) } +export function createDebugStream () { + return emitStream(debugEvents) +} + export function getDebugLog (key) { return new Promise((resolve, reject) => { let rs = debugLogFile.createReadStream() @@ -146,10 +155,6 @@ export function getDebugLog (key) { }) } -export function createDebugStream () { - return emitStream(debugEvents) -} - // read metadata for the archive, and store it in the meta db export async function pullLatestArchiveMeta (archive, {updateMTime} = {}) { try { @@ -348,6 +353,9 @@ async function loadArchiveInner (key, secretKey, userSettings = null) { await updateSizeTracking(archive) archivesDb.touch(key).catch(err => console.error('Failed to update lastAccessTime for archive', key, err)) + // attach extensions + datExtensions.attach(archive) + // store in the discovery listing, so the swarmer can find it // but not yet in the regular archives listing, because it's not fully loaded archivesByDKey[datEncoding.toStr(archive.discoveryKey)] = archive @@ -428,6 +436,7 @@ export async function unloadArchive (key) { archive.fileActStream.end() archive.fileActStream = null } + datExtensions.detach(archive) await new Promise((resolve, reject) => { archive.close(err => { if (err) reject(err) @@ -661,7 +670,8 @@ function createReplicationStream (info) { var stream = hypercoreProtocol({ id: networkId, live: true, - encrypt: true + encrypt: true, + extensions: ['ephemeral', 'session-data'] }) stream.peerInfo = info diff --git a/app/background-process/web-apis.js b/app/background-process/web-apis.js index ea42a939af..1fbed5c1c5 100644 --- a/app/background-process/web-apis.js +++ b/app/background-process/web-apis.js @@ -32,10 +32,12 @@ import datArchiveAPI from './web-apis/dat-archive' // experimental manifests import experimentalLibraryManifest from '../lib/api-manifests/external/experimental/library' import experimentalGlobalFetchManifest from '../lib/api-manifests/external/experimental/global-fetch' +import experimentalDatPeersManifest from '../lib/api-manifests/external/experimental/dat-peers' // experimental apis import experimentalLibraryAPI from './web-apis/experimental/library' import experimentalGlobalFetchAPI from './web-apis/experimental/global-fetch' +import experimentalDatPeersAPI from './web-apis/experimental/dat-peers' // exported api // = @@ -58,4 +60,5 @@ export function setup () { // experimental apis rpc.exportAPI('experimental-library', experimentalLibraryManifest, experimentalLibraryAPI, secureOnly) rpc.exportAPI('experimental-global-fetch', experimentalGlobalFetchManifest, experimentalGlobalFetchAPI, secureOnly) + rpc.exportAPI('experimental-dat-peers', experimentalDatPeersManifest, experimentalDatPeersAPI, secureOnly) } diff --git a/app/background-process/web-apis/experimental/dat-peers.js b/app/background-process/web-apis/experimental/dat-peers.js new file mode 100644 index 0000000000..c86897bef6 --- /dev/null +++ b/app/background-process/web-apis/experimental/dat-peers.js @@ -0,0 +1,77 @@ +import parseDatURL from 'parse-dat-url' +import * as datLibrary from '../../networks/dat/library' +import datDns from '../../networks/dat/dns' +import * as datExtensions from '../../networks/dat/extensions' +import {checkLabsPerm} from '../../ui/permissions' +import {DAT_HASH_REGEX} from '../../../lib/const' +import {PermissionsError} from 'beaker-error-constants' + +// constants +// = + +const API_DOCS_URL = 'https://TODO' // TODO +const API_PERM_ID = 'experimentalDatPeers' +const LAB_API_ID = 'datPeers' +const LAB_PERMS_OBJ = {perm: API_PERM_ID, labApi: LAB_API_ID, apiDocsUrl: API_DOCS_URL} + +// exported api +// = + +export default { + async list () { + await checkLabsPerm(Object.assign({sender: this.sender}, LAB_PERMS_OBJ)) + var archive = await getSenderArchive(this.sender) + return datExtensions.listPeers(archive) + }, + + async get (peerId) { + await checkLabsPerm(Object.assign({sender: this.sender}, LAB_PERMS_OBJ)) + var archive = await getSenderArchive(this.sender) + return datExtensions.getPeer(archive, peerId) + }, + + async broadcast (data) { + await checkLabsPerm(Object.assign({sender: this.sender}, LAB_PERMS_OBJ)) + var archive = await getSenderArchive(this.sender) + return datExtensions.broadcastEphemeralMessage(archive, data) + }, + + async send (peerId, data) { + await checkLabsPerm(Object.assign({sender: this.sender}, LAB_PERMS_OBJ)) + var archive = await getSenderArchive(this.sender) + return datExtensions.sendEphemeralMessage(archive, peerId, data) + }, + + async getSessionData () { + await checkLabsPerm(Object.assign({sender: this.sender}, LAB_PERMS_OBJ)) + var archive = await getSenderArchive(this.sender) + return datExtensions.getSessionData(archive) + }, + + async setSessionData (sessionData) { + await checkLabsPerm(Object.assign({sender: this.sender}, LAB_PERMS_OBJ)) + var archive = await getSenderArchive(this.sender) + return datExtensions.setSessionData(archive, sessionData) + }, + + async createEventStream () { + await checkLabsPerm(Object.assign({sender: this.sender}, LAB_PERMS_OBJ)) + var archive = await getSenderArchive(this.sender) + return datExtensions.createDatPeersStream(archive) + } +} + +// internal methods +// = + +async function getSenderArchive (sender) { + var url = sender.getURL() + if (!url.startsWith('dat:')) { + throw new PermissionsError('Only dat:// sites can use the datPeers API') + } + var urlp = parseDatURL(url) + if (!DAT_HASH_REGEX.test(urlp.host)) { + urlp.host = await datDns.resolveName(url) + } + return datLibrary.getArchive(urlp.host) +} diff --git a/app/lib/api-manifests/external/experimental/dat-peers.js b/app/lib/api-manifests/external/experimental/dat-peers.js new file mode 100644 index 0000000000..1257f94bcc --- /dev/null +++ b/app/lib/api-manifests/external/experimental/dat-peers.js @@ -0,0 +1,9 @@ +export default { + list: 'promise', + get: 'promise', + broadcast: 'promise', + send: 'promise', + getSessionData: 'promise', + setSessionData: 'promise', + createEventStream: 'readable' +} diff --git a/app/lib/perms.js b/app/lib/perms.js index c17f70c948..ca5d9ec59e 100644 --- a/app/lib/perms.js +++ b/app/lib/perms.js @@ -184,5 +184,14 @@ export default { alwaysDisallow: false, requiresRefresh: false, experimental: true - } + }, + experimentalDatPeers: { + desc: 'Send and receive messages with peers', + icon: 'exchange', + persist: true, + alwaysAllow: true, + alwaysDisallow: false, + requiresRefresh: false, + experimental: true + }, } diff --git a/app/lib/web-apis/event-target.js b/app/lib/web-apis/event-target.js index 17cbab7c55..672aab1b56 100644 --- a/app/lib/web-apis/event-target.js +++ b/app/lib/web-apis/event-target.js @@ -4,6 +4,7 @@ const LISTENERS = Symbol() const CREATE_STREAM = Symbol() const STREAM_EVENTS = Symbol() const STREAM = Symbol() +const PREP_EVENT = Symbol() export class EventTarget { constructor () { @@ -39,10 +40,11 @@ export class EventTarget { } export class EventTargetFromStream extends EventTarget { - constructor (createStreamFn, events) { + constructor (createStreamFn, events, eventPrepFn) { super() this[CREATE_STREAM] = createStreamFn this[STREAM_EVENTS] = events + this[PREP_EVENT] = eventPrepFn this[STREAM] = null } @@ -54,6 +56,9 @@ export class EventTargetFromStream extends EventTarget { this[STREAM_EVENTS].forEach(event => { s.addEventListener(event, details => { details = details || {} + if (this[PREP_EVENT]) { + details = this[PREP_EVENT](event, details) + } details.target = this this.dispatchEvent(new Event(event, details)) }) diff --git a/app/lib/web-apis/experimental.js b/app/lib/web-apis/experimental.js index 577489df70..5f7d3c6c83 100644 --- a/app/lib/web-apis/experimental.js +++ b/app/lib/web-apis/experimental.js @@ -4,6 +4,7 @@ import errors from 'beaker-error-constants' import experimentalLibraryManifest from '../api-manifests/external/experimental/library' import experimentalGlobalFetchManifest from '../api-manifests/external/experimental/global-fetch' +import experimentalDatPeersManifest from '../api-manifests/external/experimental/dat-peers' const experimental = {} const opts = {timeout: false, errors} @@ -12,6 +13,7 @@ const opts = {timeout: false, errors} if (window.location.protocol === 'beaker:' || window.location.protocol === 'dat:') { const libraryRPC = rpc.importAPI('experimental-library', experimentalLibraryManifest, opts) const globalFetchRPC = rpc.importAPI('experimental-global-fetch', experimentalGlobalFetchManifest, opts) + const datPeersRPC = rpc.importAPI('experimental-dat-peers', experimentalDatPeersManifest, opts) // experimental.library let libraryEvents = ['added', 'removed', 'updated', 'folder-synced', 'network-changed'] @@ -36,6 +38,37 @@ if (window.location.protocol === 'beaker:' || window.location.protocol === 'dat: }) return new Response(responseData.body, responseData) } + + // experimental.datPeers + class DatPeer { + constructor (id, sessionData) { + this.id = id + this.sessionData = sessionData + } + send (data) { + datPeersRPC.send(this.id, data) + } + } + function prepDatPeersEvents (event, details) { + var peer = new DatPeer(details.peerId, details.sessionData) + delete details.peerId + delete details.sessionData + details.peer = peer + return details + } + const datPeersEvents = ['connect', 'message', 'session-data', 'disconnect'] + experimental.datPeers = new EventTargetFromStream(datPeersRPC.createEventStream.bind(datPeersRPC), datPeersEvents, prepDatPeersEvents) + experimental.datPeers.list = async () => { + var peers = await datPeersRPC.list() + return peers.map(p => new DatPeer(p.id, p.sessionData)) + } + experimental.datPeers.get = async (peerId) => { + var {sessionData} = await datPeersRPC.get(peerId) + return new DatPeer(peerId, sessionData) + } + experimental.datPeers.broadcast = datPeersRPC.broadcast + experimental.datPeers.getSessionData = datPeersRPC.getSessionData + experimental.datPeers.setSessionData = datPeersRPC.setSessionData } export default experimental diff --git a/app/package.json b/app/package.json index 413b2509fa..86cd68a4d0 100644 --- a/app/package.json +++ b/app/package.json @@ -8,6 +8,8 @@ "copyright": "© 2018, Blue Link Labs", "main": "background-process.build.js", "dependencies": { + "@beaker/dat-ephemeral-ext-msg": "^0.0.2", + "@beaker/dat-session-data-ext-msg": "^1.2.0", "anymatch": "^1.3.2", "await-lock": "^1.1.2", "beaker-error-constants": "^1.4.0", From afa38fe86efd1ca0cb2d7455c158f01cfffd7c68 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 11 Jun 2018 22:23:42 -0500 Subject: [PATCH 2/5] Fix @beaker/dat-session-data-ext-msg version --- app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index 86cd68a4d0..c32ed13a24 100644 --- a/app/package.json +++ b/app/package.json @@ -9,7 +9,7 @@ "main": "background-process.build.js", "dependencies": { "@beaker/dat-ephemeral-ext-msg": "^0.0.2", - "@beaker/dat-session-data-ext-msg": "^1.2.0", + "@beaker/dat-session-data-ext-msg": "^1.1.0", "anymatch": "^1.3.2", "await-lock": "^1.1.2", "beaker-error-constants": "^1.4.0", From 1d3a73f667c0e9f41cb2779eeb2812bc89e0d91c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 12 Jun 2018 20:25:34 -0500 Subject: [PATCH 3/5] Fixes for tests --- app/background-process/test-driver.js | 23 ++++++++++++++++++----- tests/dat-archive-web-api-test.js | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/background-process/test-driver.js b/app/background-process/test-driver.js index e2286a3ece..84775f6392 100644 --- a/app/background-process/test-driver.js +++ b/app/background-process/test-driver.js @@ -47,7 +47,15 @@ const METHODS = { var page = pages.get(${page}) page.navbarEl.querySelector('.nav-location-input').value = "${url}" page.navbarEl.querySelector('.nav-location-input').blur() + var loadPromise = new Promise(resolve => { + function onDidStopLoading () { + page.webviewEl.removeEventListener('did-stop-loading', onDidStopLoading) + resolve() + } + page.webviewEl.addEventListener('did-stop-loading', onDidStopLoading) + }) page.loadURL("${url}") + loadPromise `) }, @@ -64,11 +72,16 @@ const METHODS = { }, async executeJavascriptOnPage (page, js) { - var res = await execute(` - var page = pages.get(${page}) - page.webviewEl.getWebContents().executeJavaScript(\`` + js + `\`) - `) - return res + try { + var res = await execute(` + var page = pages.get(${page}) + page.webviewEl.getWebContents().executeJavaScript(\`` + js + `\`) + `) + return res + } catch (e) { + console.error('Failed to execute javascript on page', js, e) + throw e + } } } diff --git a/tests/dat-archive-web-api-test.js b/tests/dat-archive-web-api-test.js index 15a6c72911..5e998251ab 100644 --- a/tests/dat-archive-web-api-test.js +++ b/tests/dat-archive-web-api-test.js @@ -832,7 +832,7 @@ test('archive.writeFile doesnt allow writes that exceed the quota', async t => { } }) -test.only('versioned reads and writes', async t => { +test('versioned reads and writes', async t => { // create a fresh dat var res = await app.executeJavascript(` DatArchive.create({title: 'Another Test Dat', prompt: false}) From e8a29dd63e6268d66e35b7e565ad2a45f02ab9ce Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 12 Jun 2018 20:27:17 -0500 Subject: [PATCH 4/5] Put data in e.message on peerData message event, rather than e.data --- app/background-process/networks/dat/extensions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/background-process/networks/dat/extensions.js b/app/background-process/networks/dat/extensions.js index 5c8121d83a..9f3c20246a 100644 --- a/app/background-process/networks/dat/extensions.js +++ b/app/background-process/networks/dat/extensions.js @@ -110,7 +110,7 @@ function onEphemeralMsg (archive, internalPeerObj, msg) { archive._datPeersEvents.emit('message', { peerId, sessionData: getPeerSessionData(archive, peerId), - data: decodeEphemeralMsg(msg) + message: decodeEphemeralMsg(msg) }) } From d467e4def29c0bd140b828a5ac329e11a1d49a48 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 12 Jun 2018 20:27:34 -0500 Subject: [PATCH 5/5] Add dat-peers API test --- tests/experimental-dat-peers-web-api-test.js | 251 +++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 tests/experimental-dat-peers-web-api-test.js diff --git a/tests/experimental-dat-peers-web-api-test.js b/tests/experimental-dat-peers-web-api-test.js new file mode 100644 index 0000000000..231fa9ac87 --- /dev/null +++ b/tests/experimental-dat-peers-web-api-test.js @@ -0,0 +1,251 @@ +import test from 'ava' +import os from 'os' +import path from 'path' +import fs from 'fs' +import electron from '../node_modules/electron' + +import * as browserdriver from './lib/browser-driver' + +const app1 = browserdriver.start({ + path: electron, + args: ['../app'], + env: { + NODE_ENV: 'test', + beaker_no_welcome_tab: 1, + beaker_user_data_path: fs.mkdtempSync(os.tmpdir() + path.sep + 'beaker-test-') + } +}) +const app2 = browserdriver.start({ + path: electron, + args: ['../app'], + env: { + NODE_ENV: 'test', + beaker_no_welcome_tab: 1, + beaker_user_data_path: fs.mkdtempSync(os.tmpdir() + path.sep + 'beaker-test-') + } +}) +var createdDatUrl +var mainTab1 +var mainTab2 + +test.before(async t => { + await app1.isReady + await app2.isReady + + // create the test archive + var res = await app1.executeJavascript(` + DatArchive.create({title: 'Test Archive', description: 'Foo', prompt: false}) + `) + createdDatUrl = res.url + + // go to the site + mainTab1 = app1.getTab(0) + await mainTab1.navigateTo(createdDatUrl) + mainTab2 = app2.getTab(0) + await mainTab2.navigateTo(createdDatUrl) +}) +test.after.always('cleanup', async t => { + await app1.stop() + await app2.stop() +}) + +// tests +// + +test('experiment must be opted into', async t => { + // try without experiment set + try { + await mainTab1.executeJavascript(` + experimental.datPeers.list() + `) + t.fail('Should have thrown') + } catch (e) { + t.is(e.name, 'PermissionsError') + } + + // update manifest to include experiment + await app1.executeJavascript(` + (async function () { + try { + var archive = new DatArchive("${createdDatUrl}") + var manifest = JSON.parse(await archive.readFile('dat.json', 'utf8')) + manifest.experimental = {apis: ['datPeers']} + await archive.writeFile('dat.json', JSON.stringify(manifest), 'utf8') + } catch (e) { + return e + } + return archive.readFile('dat.json', 'utf8') + })() + `) + + // make sure the change has made it to browser 2 + // await new Promise(resolve => setTimeout(resolve, 1e3)) + var manifest = await app2.executeJavascript(` + (async function () { + var archive = new DatArchive("${createdDatUrl}") + await archive.download('/dat.json') + return JSON.parse(await archive.readFile('/dat.json', 'utf8')) + })() + `) + t.deepEqual(manifest.experimental, {apis: ['datPeers']}) +}) + +test('datPeers.list() and datPeers.get()', async t => { + const listPeersCode = ` + (async function () { + var peers = await experimental.datPeers.list() + return peers + .map(p => ({id: p.id, userData: p.userData, send: typeof p.send})) + .filter(p => !!p.id) + })() + ` + const getPeersCode = (id) => ` + (async function () { + var p = await experimental.datPeers.get("${id}") + return {id: p.id, userData: p.userData, send: typeof p.send} + })() + ` + + // list peers in browser 1 + var peers1 = await mainTab1.executeJavascript(listPeersCode) + t.is(peers1.length, 1) + t.is(typeof peers1[0].id, 'string') + t.is(typeof peers1[0].userData, 'undefined') + t.is(peers1[0].send, 'function') + + // list peers in browser 2 + var peers2 = await mainTab2.executeJavascript(listPeersCode) + t.is(peers2.length, 1) + t.is(typeof peers2[0].id, 'string') + t.is(typeof peers2[0].userData, 'undefined') + t.is(peers2[0].send, 'function') + + // get peer in browser 1 + var peer1 = await mainTab1.executeJavascript(getPeersCode(peers1[0].id)) + t.is(peer1.id, peers1[0].id) + t.is(typeof peer1.userData, 'undefined') + t.is(peer1.send, 'function') + + // get peer in browser 2 + var peer2 = await mainTab2.executeJavascript(getPeersCode(peers2[0].id)) + t.is(peer2.id, peers2[0].id) + t.is(typeof peer2.userData, 'undefined') + t.is(peer2.send, 'function') +}) + +test('datPeers.broadcast() and datPeers.send()', async t => { + const listenCode = ` + window.messages = [] + experimental.datPeers.addEventListener('message', e => { + window.messages.push(e.message) + }) + ` + const broadcastCode = (value) => ` + experimental.datPeers.broadcast({foo: "${value}"}) + experimental.datPeers.broadcast("${value}") + ` + const sendCode = (value) => ` + (async function () { + var peers = await experimental.datPeers.list() + await peers[0].send({foo: "${value}"}) + await peers[0].send("${value}") + })() + ` + const getMessagesCode = ` + window.messages + ` + + // setup listeners + await mainTab1.executeJavascript(listenCode) + await mainTab2.executeJavascript(listenCode) + + // broadcast and send + await mainTab1.executeJavascript(broadcastCode('left')) + await mainTab2.executeJavascript(broadcastCode('right')) + await mainTab1.executeJavascript(sendCode('left')) + await mainTab2.executeJavascript(sendCode('right')) + + // check messages + var messages1 = await mainTab1.executeJavascript(getMessagesCode) + var messages2 = await mainTab2.executeJavascript(getMessagesCode) + t.deepEqual(messages2, [ + {foo: "left"}, + "left", + {foo: "left"}, + "left" + ]) + t.deepEqual(messages1, [ + {foo: "right"}, + "right", + {foo: "right"}, + "right" + ]) +}) + +test('datPeers.setSessionData() and datPeers.getSessionData()', async t => { + const listenCode = ` + window.sessionDatas = [] + experimental.datPeers.addEventListener('session-data', e => { + window.sessionDatas.push(e.peer.sessionData) + }) + ` + const getSessionDataCode = ` + experimental.datPeers.getSessionData() + ` + const getOtherSessionDataCode = ` + (async function () { + var peers = await experimental.datPeers.list() + return peers[0].sessionData + })() + ` + const setSessionDataCode = (value) => ` + experimental.datPeers.setSessionData(${JSON.stringify(value)}) + ` + const getSessionDatasCode = ` + window.sessionDatas + ` + + // setup listeners + await mainTab1.executeJavascript(listenCode) + await mainTab2.executeJavascript(listenCode) + + // check local session datas + t.is(await mainTab1.executeJavascript(getSessionDataCode), null) + t.is(await mainTab2.executeJavascript(getSessionDataCode), null) + + // set session datas + await mainTab1.executeJavascript(setSessionDataCode({foo: 'left'})) + await mainTab2.executeJavascript(setSessionDataCode({foo: 'right'})) + + // check local session datas + t.deepEqual(await mainTab1.executeJavascript(getSessionDataCode), {foo: 'left'}) + t.deepEqual(await mainTab2.executeJavascript(getSessionDataCode), {foo: 'right'}) + + // check other session datas + t.deepEqual(await mainTab1.executeJavascript(getOtherSessionDataCode), {foo: 'right'}) + t.deepEqual(await mainTab2.executeJavascript(getOtherSessionDataCode), {foo: 'left'}) + + // set session datas + await mainTab1.executeJavascript(setSessionDataCode('left')) + await mainTab2.executeJavascript(setSessionDataCode('right')) + + // check local session datas + t.deepEqual(await mainTab1.executeJavascript(getSessionDataCode), 'left') + t.deepEqual(await mainTab2.executeJavascript(getSessionDataCode), 'right') + + // check other session datas + t.deepEqual(await mainTab1.executeJavascript(getOtherSessionDataCode), 'right') + t.deepEqual(await mainTab2.executeJavascript(getOtherSessionDataCode), 'left') + + // check events + var sessionDatas1 = await mainTab1.executeJavascript(getSessionDatasCode) + var sessionDatas2 = await mainTab2.executeJavascript(getSessionDatasCode) + t.deepEqual(sessionDatas1, [ + {foo: "right"}, + "right" + ]) + t.deepEqual(sessionDatas2, [ + {foo: "left"}, + "left" + ]) +}) \ No newline at end of file