Further foundations for GunSmith

This commit is contained in:
Daniel Lugo 2021-09-11 14:36:19 -04:00
parent eabbb18917
commit e1e0af488c
3 changed files with 273 additions and 45 deletions

View file

@ -1,35 +1,141 @@
/** /**
* @format * @format
*/ */
/* eslint-disable no-use-before-define */
/* eslint-disable func-style */
// @ts-check // @ts-check
/// <reference path="Smith.ts" /> /// <reference path="Smith.ts" />
/// <reference path="GunT.ts" /> /// <reference path="GunT.ts" />
const uuid = require('uuid').v1
const { fork } = require('child_process') const { fork } = require('child_process')
const Config = require('./config') const Config = require('./config')
const logger = require('../../config/log')
/** /**
* Maps a path to `on()` listeners * Maps a path to `on()` listeners
* @type {Record<string, WeakSet<GunT.Listener>>} * @type {Record<string, Set<GunT.Listener>|undefined>}
*/ */
const pathToListeners = { const pathToListeners = {}
'x>x>x>': new WeakSet()
} /**
* Path to pending puts. Oldest to newest
* @type {Record<string, Smith.PendingPut[]?>}
*/
const pendingPuts = {}
/** /**
* @param {Smith.GunMsg} msg * @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 currentGun = fork('./gun')
let lastAlias = ''
let lastPass = ''
/**
* @param {string} alias
* @param {string} pass
* @returns {Promise<string>}
*/
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<string>}
*/
const autoAuth = () => {
if (!lastAlias || !lastPass) {
return Promise.resolve('')
}
return auth(lastAlias, lastPass)
}
const processPendingPutsFromLastGun = async (forGun, pps = pendingPuts) => {
// TODO
}
const forge = () => { const forge = () => {
currentGun.off('message', handleMsg)
currentGun.kill() currentGun.kill()
currentGun = fork('./gun') const newGun = fork('./gun')
currentGun = newGun
currentGun.on('message', handleMsg) currentGun.on('message', handleMsg)
const msgs = Object.keys(pathToListeners).map(path => { const lastGunListeners = Object.keys(pathToListeners).map(path => {
/** @type {Smith.SmithMsgOn} */ /** @type {Smith.SmithMsgOn} */
const msg = { const msg = {
path, path,
@ -38,7 +144,11 @@ const forge = () => {
return msg return msg
}) })
currentGun.send(msgs) currentGun.send(lastGunListeners)
autoAuth().then(() => {
processPendingPutsFromLastGun(newGun)
})
} }
/** /**
@ -46,7 +156,7 @@ const forge = () => {
* @param {boolean=} afterMap * @param {boolean=} afterMap
* @returns {GunT.GUNNode} * @returns {GunT.GUNNode}
*/ */
const createReplica = (path, afterMap = false) => { function createReplica(path, afterMap = false) {
/** @type {GunT.Listener[]} */ /** @type {GunT.Listener[]} */
const listenersForThisRef = [] const listenersForThisRef = []
@ -59,36 +169,112 @@ const createReplica = (path, afterMap = false) => {
put: {} put: {}
}, },
get(key) { 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) return createReplica(path + '>' + key)
}, },
map() { map() {
if (afterMap) {
throw new Error('Cannot call map() after map() on a GunSmith node')
}
return createReplica(path, true) return createReplica(path, true)
}, },
off() { off() {
if (afterMap) {
throw new Error('Cannot call off() after map() on a GunSmith node')
}
for (const l of listenersForThisRef) { 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) { on(cb) {
listenersForThisRef.push(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} */ /** @type {Smith.SmithMsgOn} */
const msg = { const msg = {
path, path,
type: 'on' type: 'on'
} }
currentGun.send(msg) currentGun.send(msg)
return this 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 return this
}, },
put(data, cb) { 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 return this
}, },
set(data, cb) { set(data, cb) {
if (afterMap) {
throw new Error('Cannot call set() after map() on a GunSmith node')
}
return this return this
}, },
then() { then() {
if (afterMap) {
throw new Error('Cannot call then() after map() on a GunSmith node')
}
return Promise.resolve(null) return Promise.resolve(null)
}, },
user(pub) { user(pub) {
@ -98,35 +284,34 @@ const createReplica = (path, afterMap = false) => {
) )
} }
if (!pub) { if (!pub) {
throw new Error( return createUserReplica()
'Do not use `user()` (without providing a pub) on a GunSmith node, use getUser()'
)
} }
const replica = createReplica(pub) const replica = createReplica(pub)
// I don't know why Typescript insists on returning a UserGUNNode so here we go:
return { return {
...replica, ...replica,
_: { /** @returns {GunT.UserSoul} */
...replica._, get _() {
sea: { throw new ReferenceError(
epriv: 'Do not use ._.sea on a GunSmith node, use getUser()', `Do not access _ on another user's graph (${pub.slice(
epub: 'Do not use ._.sea on a GunSmith node, use getUser()', 0,
priv: 'Do not use ._.sea on a GunSmith node, use getUser()', 8
pub: 'Do not use ._.sea on a GunSmith node, use getUser()' )}...${pub.slice(-8)})`
} )
}, },
auth() { auth() {
throw new Error( 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() { create() {
throw new Error( 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() { leave() {
throw new Error( 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) => {
} }
} }
/** let userReplicaCalled = false
* @returns {GunT.GUNNode}
*/
const getGun = () => createReplica('$root')
/** /**
* @returns {GunT.UserGUNNode} * @returns {GunT.UserGUNNode}
*/ */
const getUser = createReplica('$user') function createUserReplica() {
if (userReplicaCalled) {
module.exports = { throw new Error('Please only call gun.user() (without a pub) once.')
getGun, }
getUser userReplicaCalled = true
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
} }

View file

@ -114,7 +114,7 @@ namespace GunT {
export interface UserGUNNode extends GUNNode { export interface UserGUNNode extends GUNNode {
_: UserSoul _: UserSoul
auth(user: string, pass: string, cb: AuthCB): void auth(alias: string, pass: string, cb: AuthCB): void
is?: { is?: {
alias: string alias: string
pub: string pub: string

View file

@ -3,6 +3,19 @@
*/ */
/// <reference path="GunT.ts" /> /// <reference path="GunT.ts" />
namespace Smith { 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 { export interface SmithMsgOn {
path: string path: string
type: 'on' type: 'on'
@ -16,27 +29,24 @@ namespace Smith {
export interface SmithMsgPut { export interface SmithMsgPut {
id: string id: string
data: GunT.ValidDataValue
path: string path: string
type: 'put' type: 'put'
value: any
} }
export type SmithMsg = SmithMsgOn | SmithMsgOnce | SmithMsgPut | BatchSmithMsg export type SmithMsg = SmithMsgOn | SmithMsgOnce | SmithMsgPut | BatchSmithMsg
export type BatchSmithMsg = SmithMsg[] export type BatchSmithMsg = SmithMsg[]
export interface GunMsgOn { export interface GunMsgAuth {
data: any ack: GunT.AuthAck
key: string type: 'auth'
path: string
type: 'on'
} }
export interface GunMsgOnce { export interface GunMsgOn {
data: any data: any
id: string path: string
key: string type: 'on'
type: 'once'
} }
export interface GunMsgPut { export interface GunMsgPut {
@ -46,5 +56,5 @@ namespace Smith {
type: 'put' type: 'put'
} }
export type GunMsg = GunMsgOn | GunMsgOnce | GunMsgPut export type GunMsg = GunMsgAuth| GunMsgOn | GunMsgPut
} }