Gun Smith foundations

This commit is contained in:
Daniel Lugo 2021-09-09 17:39:51 -04:00
parent 7133d97749
commit fb25746072
6 changed files with 513 additions and 0 deletions

154
utils/GunSmith/GunT.ts Normal file
View file

@ -0,0 +1,154 @@
/**
* @prettier
*/
namespace GunT {
export type Primitive = boolean | string | number
export interface Data {
[K: string]: ValidDataValue
}
export type ValidDataValue = Primitive | null | Data
export interface Ack {
err: string | undefined
}
type ListenerObjSoul = {
'#': string
}
export type ListenerObj = Record<
string,
ListenerObjSoul | Primitive | null
> & {
_: ListenerObjSoul
}
export type ListenerData = Primitive | null | ListenerObj | undefined
interface OpenListenerDataObj {
[k: string]: OpenListenerData
}
export type Listener = (data: ListenerData, key: string) => void
export type Callback = (ack: Ack) => void
export interface Peer {
url: string
id: string
wire?: {
readyState: number
}
}
export interface Soul {
get: string
put: Primitive | null | object | undefined
opt: {
peers: Record<string, Peer>
}
}
export type OpenListenerData = Primitive | null | OpenListenerDataObj
export type OpenListener = (data: OpenListenerData, key: string) => void
export type LoadListenerData = OpenListenerData
export type LoadListener = (data: LoadListenerData, key: string) => void
export interface GUNNodeBase {
_: Soul
map(): GUNNode
on(this: GUNNode, cb: Listener): void
once(this: GUNNode, cb?: Listener, opts?: { wait: number }): GUNNode
// open(this: GUNNode, cb?: OpenListener): GUNNode
// load(this: GUNNode, cb?: OpenListener): GUNNode
// load(this: GUNNode, cb?: LoadListener): GUNNode
off(): void
user(): UserGUNNode
user(pub: string): GUNNode
then(): Promise<ListenerData>
then<T>(cb: (v: ListenerData) => T): Promise<ListenerData>
}
export interface GUNNode extends GUNNodeBase {
get(key: string): GUNNode
put(data: ValidDataValue, cb?: Callback): GUNNode
set(data: ValidDataValue, cb?: Callback): GUNNode
}
export interface CreateAck {
pub: string | undefined
err: string | undefined
}
export type CreateCB = (ack: CreateAck) => void
export interface AuthAck {
err: string | undefined
sea:
| {
pub: string
}
| undefined
}
export type AuthCB = (ack: AuthAck) => void
export interface UserPair {
epriv: string
epub: string
priv: string
pub: string
}
export interface UserSoul extends Soul {
sea: UserPair
}
export interface UserGUNNode extends GUNNode {
_: UserSoul
auth(user: string, pass: string, cb: AuthCB): void
is?: {
alias: string
pub: string
}
create(user: string, pass: string, cb: CreateCB): void
leave(): void
}
export interface ISEA {
encrypt(
message: string | number | boolean,
senderSecret: string
): Promise<string>
decrypt(encryptedMessage: string, recipientSecret: string): Promise<string>
decryptNumber(
encryptedMessage: string,
recipientSecret: string
): Promise<number>
decryptBoolean(
encryptedMessage: string,
recipientSecret: string
): Promise<boolean>
secret(
recipientOrSenderEpub: string,
recipientOrSenderUserPair: UserPair
): Promise<string>
}
export interface MySEA {
encrypt(message: string, senderSecret: string): Promise<string>
decrypt(encryptedMessage: string, recipientSecret: string): Promise<string>
secret(
recipientOrSenderEpub: string,
recipientOrSenderUserPair: UserPair
): Promise<string>
}
}

50
utils/GunSmith/Smith.ts Normal file
View file

@ -0,0 +1,50 @@
/**
* @format
*/
/// <reference path="GunT.ts" />
namespace Smith {
export interface SmithMsgOn {
path: string
type: 'on'
}
export interface SmithMsgOnce {
id: string
path: string
type: 'once'
}
export interface SmithMsgPut {
id: string
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 GunMsgOnce {
data: any
id: string
key: string
type: 'once'
}
export interface GunMsgPut {
ack: GunT.Ack
id: string
path: string
type: 'put'
}
export type GunMsg = GunMsgOn | GunMsgOnce | GunMsgPut
}

25
utils/GunSmith/config.js Normal file
View file

@ -0,0 +1,25 @@
/**
* @format
*/
/* eslint-disable no-process-env */
const dotenv = require('dotenv')
const defaults = require('../../config/defaults')(false)
dotenv.config()
// @ts-ignore Let it crash if undefined
exports.DATA_FILE_NAME = process.env.DATA_FILE_NAME || defaults.dataFileName
/**
* @type {string[]}
*/
exports.PEERS = process.env.PEERS
? JSON.parse(process.env.PEERS)
: defaults.peers
exports.MS_TO_TOKEN_EXPIRATION = Number(
process.env.MS_TO_TOKEN_EXPIRATION || defaults.tokenExpirationMS
)
exports.SHOW_LOG = process.env.SHOW_GUN_DB_LOG === 'true'

133
utils/GunSmith/gun.js Normal file
View file

@ -0,0 +1,133 @@
/**
* @format
*/
// @ts-check
/// <reference path="Smith.ts" />
/// <reference path="GunT.ts" />
const Gun = require('gun')
// @ts-ignore
require('gun/nts')
const Config = require('./config')
// @ts-ignore
Gun.log = () => {}
/**
* This var is just to please typescript's casting rules.
*/
const _gun = /** @type {any} */ (new Gun({
axe: false,
multicast: false,
peers: Config.PEERS
}))
/**
* @type {GunT.GUNNode}
*/
const gun = _gun
const user = gun.user()
/** @type {Set<string>} */
const pendingOnces = new Set()
/**
* @returns {Promise<void>}
*/
const waitForAuth = async () => {
if (user.is?.pub) {
return Promise.resolve()
}
await new Promise(res => setTimeout(res, 1000))
return waitForAuth()
}
/**
* @param {Smith.SmithMsg} msg
*/
const handleMsg = msg => {
if (Array.isArray(msg)) {
msg.forEach(handleMsg)
return
}
if (msg.type === 'on') {
const [root, ...keys] = msg.path.split('>')
/** @type {GunT.GUNNode} */
let node =
{
$root: gun,
$user: user
}[root] || gun.user(root)
for (const key of keys) {
node = node.get(key)
}
node.on((data, key) => {
/** @type {Smith.GunMsgOn} */
const res = {
data,
key,
path: msg.path,
type: 'on'
}
// @ts-expect-error
process.send(res)
})
}
if (msg.type === 'once') {
const [root, ...keys] = msg.path.split('>')
/** @type {GunT.GUNNode} */
let node =
{
$root: gun,
$user: user
}[root] || gun.user(root)
for (const key of keys) {
node = node.get(key)
}
node.once((data, key) => {
/** @type {Smith.GunMsgOnce} */
const res = {
data,
id: msg.id,
key,
type: 'once'
}
// @ts-expect-error
process.send(res)
})
}
if (msg.type === 'put') {
const [root, ...keys] = msg.path.split('>')
/** @type {GunT.GUNNode} */
let node =
{
$root: gun,
$user: user
}[root] || gun.user(root)
for (const key of keys) {
node = node.get(key)
}
node.on((data, key) => {
/** @type {Smith.GunMsgOn} */
const res = {
data,
key,
path: msg.path,
type: 'on'
}
// @ts-expect-error
process.send(res)
})
}
}
process.on('message', handleMsg)

150
utils/GunSmith/gunSmith.js Normal file
View file

@ -0,0 +1,150 @@
/**
* @format
*/
// @ts-check
/// <reference path="Smith.ts" />
/// <reference path="GunT.ts" />
const { fork } = require('child_process')
const Config = require('./config')
/**
* Maps a path to `on()` listeners
* @type {Record<string, WeakSet<GunT.Listener>>}
*/
const pathToListeners = {
'x>x>x>': new WeakSet()
}
/**
* @param {Smith.GunMsg} msg
*/
const handleMsg = msg => {}
let currentGun = fork('./gun')
const forge = () => {
currentGun.kill()
currentGun = fork('./gun')
currentGun.on('message', handleMsg)
const msgs = Object.keys(pathToListeners).map(path => {
/** @type {Smith.SmithMsgOn} */
const msg = {
path,
type: 'on'
}
return msg
})
currentGun.send(msgs)
}
/**
* @param {string} path
* @param {boolean=} afterMap
* @returns {GunT.GUNNode}
*/
const createReplica = (path, afterMap = false) => {
/** @type {GunT.Listener[]} */
const listenersForThisRef = []
return {
_: {
get: '',
opt: {
peers: {}
},
put: {}
},
get(key) {
return createReplica(path + '>' + key)
},
map() {
return createReplica(path, true)
},
off() {
for (const l of listenersForThisRef) {
pathToListeners[path].delete(l)
}
},
on(cb) {
listenersForThisRef.push(cb)
/** @type {Smith.SmithMsgOn} */
const msg = {
path,
type: 'on'
}
currentGun.send(msg)
return this
},
once(cb, opts) {
return this
},
put(data, cb) {
return this
},
set(data, cb) {
return this
},
then() {
return Promise.resolve(null)
},
user(pub) {
if (path !== '$root') {
throw new ReferenceError(
`Do not call user() on a non-root GunSmith node`
)
}
if (!pub) {
throw new Error(
'Do not use `user()` (without providing a pub) on a GunSmith node, use getUser()'
)
}
const replica = createReplica(pub)
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()'
}
},
auth() {
throw new Error(
'Do not call auth() on a GunSmith node, use getUser()'
)
},
create() {
throw new Error(
'Do not call create() on a GunSmith node, use getUser()'
)
},
leave() {
throw new Error(
'Do not call leave() on a GunSmith node, use getUser()'
)
}
}
}
}
}
/**
* @returns {GunT.GUNNode}
*/
const getGun = () => createReplica('$root')
/**
* @returns {GunT.UserGUNNode}
*/
const getUser = createReplica('$user')
module.exports = {
getGun,
getUser
}

1
utils/GunSmith/index.js Normal file
View file

@ -0,0 +1 @@
module.exports = require('./GunSmith')