From e1e0af488c09bf22445a4c348d4b40c96717bdd4 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Sat, 11 Sep 2021 14:36:19 -0400 Subject: [PATCH] Further foundations for GunSmith --- utils/GunSmith/GunSmith.js | 284 ++++++++++++++++++++++++++++++++----- utils/GunSmith/GunT.ts | 2 +- utils/GunSmith/Smith.ts | 32 +++-- 3 files changed, 273 insertions(+), 45 deletions(-) diff --git a/utils/GunSmith/GunSmith.js b/utils/GunSmith/GunSmith.js index 9c1e2d14..288185ba 100644 --- a/utils/GunSmith/GunSmith.js +++ b/utils/GunSmith/GunSmith.js @@ -1,35 +1,141 @@ /** * @format */ +/* eslint-disable no-use-before-define */ +/* eslint-disable func-style */ // @ts-check /// /// +const uuid = require('uuid').v1 const { fork } = require('child_process') const Config = require('./config') +const logger = require('../../config/log') + /** * Maps a path to `on()` listeners - * @type {Record>} + * @type {Record|undefined>} */ -const pathToListeners = { - 'x>x>x>': new WeakSet() -} +const pathToListeners = {} + +/** + * Path to pending puts. Oldest to newest + * @type {Record} + */ +const pendingPuts = {} /** * @param {Smith.GunMsg} msg */ -const handleMsg = msg => {} +const handleMsg = msg => { + if (msg.type === 'on') { + const { data, path } = msg + + // eslint-disable-next-line no-multi-assign + const listeners = (pathToListeners[path] = + pathToListeners[path] || new Set()) + + for (const l of listeners) { + l(data, path.split('>')[path.split('>').length - 1]) + } + } + if (msg.type === 'put') { + const { ack, id, path } = msg + + const pendingPutsForPath = pendingPuts[path] || (pendingPuts[path] = []) + + const pendingPut = pendingPutsForPath.find(pp => pp.id === id) + const idx = pendingPutsForPath.findIndex(pp => pp.id === id) + + if (pendingPut) { + if (pendingPut.cb) { + pendingPut.cb(ack) + } + pendingPutsForPath.splice(idx, 1) + } else { + logger.error( + `Could not find request for put message from gun subprocess. Data will be logged below.` + ) + logger.info({ msg, pendingPut }) + } + } +} let currentGun = fork('./gun') +let lastAlias = '' +let lastPass = '' + +/** + * @param {string} alias + * @param {string} pass + * @returns {Promise} + */ +const auth = (alias, pass) => + new Promise((res, rej) => { + /** @type {Smith.SmithMsgAuth} */ + const msg = { + alias, + pass, + type: 'auth' + } + /** @param {Smith.GunMsg} msg */ + const _cb = msg => { + if (msg.type === 'auth') { + currentGun.off('message', _cb) + + const { ack } = msg + + if (ack.err) { + rej( + new Error( + + ack.err + ) + ) + } else if (ack.sea) { + lastAlias = alias; + lastPass = pass + res(ack.sea.pub) + } else { + rej( + new Error( + 'Auth: ack.sea undefined' + ) + ) + } + } + } + currentGun.on('message', _cb) + currentGun.send(msg) + }) + + + +/** + * @returns {Promise} + */ +const autoAuth = () => { + if (!lastAlias || !lastPass) { + return Promise.resolve('') + } + return auth(lastAlias, lastPass) +} + +const processPendingPutsFromLastGun = async (forGun, pps = pendingPuts) => { + // TODO +} + const forge = () => { + currentGun.off('message', handleMsg) currentGun.kill() - currentGun = fork('./gun') + const newGun = fork('./gun') + currentGun = newGun currentGun.on('message', handleMsg) - const msgs = Object.keys(pathToListeners).map(path => { + const lastGunListeners = Object.keys(pathToListeners).map(path => { /** @type {Smith.SmithMsgOn} */ const msg = { path, @@ -38,7 +144,11 @@ const forge = () => { return msg }) - currentGun.send(msgs) + currentGun.send(lastGunListeners) + + autoAuth().then(() => { + processPendingPutsFromLastGun(newGun) + }) } /** @@ -46,7 +156,7 @@ const forge = () => { * @param {boolean=} afterMap * @returns {GunT.GUNNode} */ -const createReplica = (path, afterMap = false) => { +function createReplica(path, afterMap = false) { /** @type {GunT.Listener[]} */ const listenersForThisRef = [] @@ -59,36 +169,112 @@ const createReplica = (path, afterMap = false) => { put: {} }, get(key) { + if (afterMap) { + throw new Error( + 'Cannot call get() after map() on a GunSmith node, you should only call on() after map()' + ) + } return createReplica(path + '>' + key) }, map() { + if (afterMap) { + throw new Error('Cannot call map() after map() on a GunSmith node') + } return createReplica(path, true) }, off() { + if (afterMap) { + throw new Error('Cannot call off() after map() on a GunSmith node') + } for (const l of listenersForThisRef) { - pathToListeners[path].delete(l) + // eslint-disable-next-line no-multi-assign + const listeners = (pathToListeners[path] = + pathToListeners[path] || new Set()) + + listeners.delete(l) } }, on(cb) { listenersForThisRef.push(cb) + + // eslint-disable-next-line no-multi-assign + const listeners = (pathToListeners[path] = + pathToListeners[path] || new Set()) + + listeners.add(cb) + /** @type {Smith.SmithMsgOn} */ const msg = { path, type: 'on' } currentGun.send(msg) + return this }, - once(cb, opts) { + once(cb, opts = { wait: 200 }) { + const tmp = createReplica(path, afterMap) + if (afterMap) { + // + } else { + let lastVal = null + + tmp.on((data, key) => { + lastVal = data + }) + + setTimeout(() => { + if (cb) { + cb(lastVal, path.split('>').lastItem) + } + }, opts.wait) + } return this }, put(data, cb) { + const id = uuid() + + const pendingPutsForPath = pendingPuts[path] || (pendingPuts[path] = []) + + /** @type {Smith.PendingPut} */ + const pendingPut = { + cb(ack) { + const idx = pendingPutsForPath.indexOf(this) + if (idx > -1) { + pendingPutsForPath.splice(idx, 1) + } else { + logger.warn(`???`) + } + // eslint-disable-next-line no-unused-expressions + cb && cb(ack) + }, + data, + id + } + + pendingPutsForPath.push(pendingPut) + + /** @type {Smith.SmithMsgPut} */ + const msg = { + data, + id, + path, + type: 'put' + } + + currentGun.send(msg) return this }, set(data, cb) { + if (afterMap) { + throw new Error('Cannot call set() after map() on a GunSmith node') + } return this }, then() { + if (afterMap) { + throw new Error('Cannot call then() after map() on a GunSmith node') + } return Promise.resolve(null) }, user(pub) { @@ -98,35 +284,34 @@ const createReplica = (path, afterMap = false) => { ) } if (!pub) { - throw new Error( - 'Do not use `user()` (without providing a pub) on a GunSmith node, use getUser()' - ) + return createUserReplica() } const replica = createReplica(pub) + // I don't know why Typescript insists on returning a UserGUNNode so here we go: return { ...replica, - _: { - ...replica._, - sea: { - epriv: 'Do not use ._.sea on a GunSmith node, use getUser()', - epub: 'Do not use ._.sea on a GunSmith node, use getUser()', - priv: 'Do not use ._.sea on a GunSmith node, use getUser()', - pub: 'Do not use ._.sea on a GunSmith node, use getUser()' - } + /** @returns {GunT.UserSoul} */ + get _() { + throw new ReferenceError( + `Do not access _ on another user's graph (${pub.slice( + 0, + 8 + )}...${pub.slice(-8)})` + ) }, auth() { throw new Error( - 'Do not call auth() on a GunSmith node, use getUser()' + "Do not call auth() on another user's graph (gun.user(otherUserPub))" ) }, create() { throw new Error( - 'Do not call create() on a GunSmith node, use getUser()' + "Do not call create() on another user's graph (gun.user(otherUserPub))" ) }, leave() { throw new Error( - 'Do not call leave() on a GunSmith node, use getUser()' + "Do not call leave() on another user's graph (gun.user(otherUserPub))" ) } } @@ -134,17 +319,50 @@ const createReplica = (path, afterMap = false) => { } } -/** - * @returns {GunT.GUNNode} - */ -const getGun = () => createReplica('$root') +let userReplicaCalled = false /** * @returns {GunT.UserGUNNode} */ -const getUser = createReplica('$user') +function createUserReplica() { + if (userReplicaCalled) { + throw new Error('Please only call gun.user() (without a pub) once.') + } + userReplicaCalled = true -module.exports = { - getGun, - getUser + const baseReplica = createReplica('$user') + + /** @type {GunT.UserGUNNode} */ + const completeReplica = { + ...baseReplica, + _: { + ...baseReplica._, + // TODO + sea: { + epriv: '', + epub: '', + priv: '', + pub: '' + } + }, + auth(alias, pass, cb) { + auth(alias, pass).then((pub) => { + cb({ + err: undefined, + sea: { + pub, + } + }) + }).catch(e => { + cb({ + err: e.message, + sea: undefined + }) + }) + }, + create() {}, + leave() {} + } + + return completeReplica } diff --git a/utils/GunSmith/GunT.ts b/utils/GunSmith/GunT.ts index b3a74409..a4c6ef32 100644 --- a/utils/GunSmith/GunT.ts +++ b/utils/GunSmith/GunT.ts @@ -114,7 +114,7 @@ namespace GunT { export interface UserGUNNode extends GUNNode { _: UserSoul - auth(user: string, pass: string, cb: AuthCB): void + auth(alias: string, pass: string, cb: AuthCB): void is?: { alias: string pub: string diff --git a/utils/GunSmith/Smith.ts b/utils/GunSmith/Smith.ts index e17910e7..1988e96a 100644 --- a/utils/GunSmith/Smith.ts +++ b/utils/GunSmith/Smith.ts @@ -3,6 +3,19 @@ */ /// namespace Smith { + + export interface PendingPut { + cb: GunT.Callback + data: GunT.ValidDataValue + id: string + } + + export interface SmithMsgAuth { + alias: string + pass: string + type: 'auth' + } + export interface SmithMsgOn { path: string type: 'on' @@ -16,27 +29,24 @@ namespace Smith { export interface SmithMsgPut { id: string + data: GunT.ValidDataValue path: string type: 'put' - value: any } export type SmithMsg = SmithMsgOn | SmithMsgOnce | SmithMsgPut | BatchSmithMsg export type BatchSmithMsg = SmithMsg[] - export interface GunMsgOn { - data: any - key: string - path: string - type: 'on' + export interface GunMsgAuth { + ack: GunT.AuthAck + type: 'auth' } - export interface GunMsgOnce { + export interface GunMsgOn { data: any - id: string - key: string - type: 'once' + path: string + type: 'on' } export interface GunMsgPut { @@ -46,5 +56,5 @@ namespace Smith { type: 'put' } - export type GunMsg = GunMsgOn | GunMsgOnce | GunMsgPut + export type GunMsg = GunMsgAuth| GunMsgOn | GunMsgPut }