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
*/
/* eslint-disable no-use-before-define */
/* eslint-disable func-style */
// @ts-check
/// <reference path="Smith.ts" />
/// <reference path="GunT.ts" />
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<string, WeakSet<GunT.Listener>>}
* @type {Record<string, Set<GunT.Listener>|undefined>}
*/
const pathToListeners = {
'x>x>x>': new WeakSet()
}
const pathToListeners = {}
/**
* Path to pending puts. Oldest to newest
* @type {Record<string, Smith.PendingPut[]?>}
*/
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<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 = () => {
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
}

View file

@ -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

View file

@ -3,6 +3,19 @@
*/
/// <reference path="GunT.ts" />
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
}