persist app sharing
This commit is contained in:
parent
312abc11d8
commit
d1e2e7d7a4
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
data/
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
import { apps } from "app/src/webapp"
|
||||
import { connectSneaker, sneakers, sneakerUrl } from "app/src/sneaker"
|
||||
|
||||
export default async function (app: string, subdomain = "") {
|
||||
export default async function (app: string) {
|
||||
if (!app) {
|
||||
let out = `usage: share <app> [subdomain]`
|
||||
const apps = sneakers()
|
||||
if (apps.length) {
|
||||
out += "\n\nUse `unshare` to stop sharing an app."
|
||||
out += "\n\nsharing\n" + apps.map(app => {
|
||||
const url = sneakerUrl(app)
|
||||
return `${app}: <a href="${url}">${url}</a>`
|
||||
|
|
@ -20,6 +21,6 @@ export default async function (app: string, subdomain = "") {
|
|||
return { error: `${app} not found` }
|
||||
}
|
||||
|
||||
const url = sneakerUrl(await connectSneaker(app, subdomain))
|
||||
const url = sneakerUrl(await connectSneaker(app))
|
||||
return { html: `<a href="${url}">${url}</a>` }
|
||||
}
|
||||
6
app/nose/bin/state.ts
Normal file
6
app/nose/bin/state.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { NOSE_DATA } from "@/config"
|
||||
import { join } from "path"
|
||||
|
||||
export default async function () {
|
||||
return JSON.parse(await Bun.file(join(NOSE_DATA, "state.json")).text())
|
||||
}
|
||||
|
|
@ -10,5 +10,7 @@ export const NOSE_DIR = resolve("..")
|
|||
export const NOSE_BIN = join(NOSE_DIR, "bin")
|
||||
export const NOSE_WWW = join(NOSE_DIR, "www")
|
||||
|
||||
export const NOSE_DATA = resolve("./data")
|
||||
|
||||
export const NOSE_STARTED = Date.now()
|
||||
export const GIT_SHA = (await $`git rev-parse HEAD`.text()).trim()
|
||||
27
app/src/mutex.ts
Normal file
27
app/src/mutex.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
////
|
||||
// Simple mutex for concurrent file writes
|
||||
|
||||
export class Mutex {
|
||||
private queue: (() => void)[] = []
|
||||
private locked = false
|
||||
|
||||
async lock(): Promise<() => void> {
|
||||
return new Promise(resolve => {
|
||||
const unlock = () => {
|
||||
const next = this.queue.shift()
|
||||
if (next) {
|
||||
next()
|
||||
} else {
|
||||
this.locked = false
|
||||
}
|
||||
}
|
||||
|
||||
if (this.locked) {
|
||||
this.queue.push(() => resolve(unlock))
|
||||
} else {
|
||||
this.locked = true
|
||||
resolve(unlock)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -7,17 +7,17 @@ import { prettyJSON } from "hono/pretty-json"
|
|||
import color from "kleur"
|
||||
|
||||
import type { Message } from "./shared/types"
|
||||
import { NOSE_ICON, NOSE_BIN, NOSE_WWW } from "./config"
|
||||
import { NOSE_ICON, NOSE_BIN, NOSE_WWW, NOSE_DATA } from "./config"
|
||||
import { transpile, isFile, tilde } from "./utils"
|
||||
import { serveApp } from "./webapp"
|
||||
import { initDNS } from "./dns"
|
||||
import { commands, commandSource, commandPath } from "./commands"
|
||||
import { commands, commandPath } from "./commands"
|
||||
import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket"
|
||||
|
||||
import { Layout } from "./html/layout"
|
||||
import { Terminal } from "./html/terminal"
|
||||
import { dispatchMessage } from "./dispatch"
|
||||
import "./sneaker"
|
||||
import { initSneakers, disconnectSneakers } from "./sneaker"
|
||||
|
||||
//
|
||||
// Hono setup
|
||||
|
|
@ -129,6 +129,7 @@ if (process.env.BUN_HOT) {
|
|||
// @ts-ignore
|
||||
globalThis.__hot_reload_cleanup = () => {
|
||||
closeWebsockets()
|
||||
disconnectSneakers()
|
||||
}
|
||||
|
||||
for (const sig of ["SIGINT", "SIGTERM"] as const) {
|
||||
|
|
@ -138,19 +139,27 @@ if (process.env.BUN_HOT) {
|
|||
process.exit(0)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
initDNS()
|
||||
}
|
||||
|
||||
//
|
||||
// production mode
|
||||
//
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
initDNS()
|
||||
}
|
||||
|
||||
//
|
||||
// server start
|
||||
//
|
||||
|
||||
console.log(color.cyan(NOSE_ICON))
|
||||
console.log(color.blue("NOSE_DATA:"), color.yellow(tilde(NOSE_DATA)))
|
||||
console.log(color.blue("NOSE_BIN:"), color.yellow(tilde(NOSE_BIN)))
|
||||
console.log(color.blue("NOSE_WWW:"), color.yellow(tilde(NOSE_WWW)))
|
||||
|
||||
initSneakers()
|
||||
|
||||
export default {
|
||||
port: process.env.PORT || 3000,
|
||||
hostname: "0.0.0.0",
|
||||
|
|
|
|||
|
|
@ -83,5 +83,5 @@ function errorMessage(error: Error | any): string {
|
|||
}
|
||||
|
||||
function isJSX(obj: any): boolean {
|
||||
return 'tag' in obj && 'props' in obj && 'children' in obj
|
||||
return typeof obj === 'object' && 'tag' in obj && 'props' in obj && 'children' in obj
|
||||
}
|
||||
|
|
@ -3,12 +3,15 @@
|
|||
// with the public internet. It requires a sneaker server, usually hosted by us.
|
||||
|
||||
import nose from "./server"
|
||||
import { clearState, setState, getState } from "app/src/state"
|
||||
|
||||
const SNEAKER_URL = "nose.space"
|
||||
const SNEAKER_TLS = true
|
||||
// const SNEAKER_URL = "localhost:3100"
|
||||
// const SNEAKER_TLS = false
|
||||
|
||||
const PREFIX = "sneaker:"
|
||||
|
||||
type Connection = {
|
||||
subdomain: string
|
||||
ws: any
|
||||
|
|
@ -16,6 +19,19 @@ type Connection = {
|
|||
}
|
||||
const conns: Record<string, Connection> = {}
|
||||
|
||||
export function initSneakers() {
|
||||
const state = getState()
|
||||
if (!state) return
|
||||
|
||||
for (const key in state) {
|
||||
if (key.startsWith(PREFIX)) {
|
||||
const app = key.replace(PREFIX, "")
|
||||
console.log("sharing", app, state[key])
|
||||
connectSneaker(app, state[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function sneakerUrl(appOrSubdomain: string): string {
|
||||
let conn = conns[appOrSubdomain]
|
||||
if (!conn) {
|
||||
|
|
@ -29,7 +45,6 @@ export function sneakerUrl(appOrSubdomain: string): string {
|
|||
return "none"
|
||||
}
|
||||
|
||||
|
||||
let url = "http" + (SNEAKER_TLS ? "s" : "") + "://"
|
||||
url += conn.subdomain + "." + SNEAKER_URL
|
||||
|
||||
|
|
@ -40,10 +55,19 @@ export function sneakers(): string[] {
|
|||
return Object.keys(conns)
|
||||
}
|
||||
|
||||
export function disconnectSneaker(app: string): boolean {
|
||||
export async function disconnectSneakers() {
|
||||
for (const app in conns) {
|
||||
const conn = conns[app]!
|
||||
conn.close = true
|
||||
conn.ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnectSneaker(app: string): Promise<boolean> {
|
||||
if (!sneakers().includes(app) || !conns[app])
|
||||
return false
|
||||
|
||||
await clearState(`${PREFIX}${app}`)
|
||||
conns[app].close = true
|
||||
conns[app].ws.close()
|
||||
|
||||
|
|
@ -76,6 +100,7 @@ export async function connectSneaker(app: string, subdomain = ""): Promise<strin
|
|||
|
||||
if (msg.subdomain) {
|
||||
conns[app] = { subdomain: msg.subdomain, ws, close: false }
|
||||
await setState(`${PREFIX}${app}`, msg.subdomain)
|
||||
subdomain = msg.subdomain
|
||||
resolve(msg.subdomain)
|
||||
return
|
||||
|
|
|
|||
42
app/src/state.ts
Normal file
42
app/src/state.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
////
|
||||
// Server state, shared by all sessions.
|
||||
|
||||
import { join } from "path"
|
||||
import { readFileSync } from "fs"
|
||||
import { NOSE_DATA } from "./config"
|
||||
import { Mutex } from "./mutex"
|
||||
|
||||
const statePath = join(NOSE_DATA, "state.json")
|
||||
const mutex = new Mutex()
|
||||
|
||||
export function getState(key?: string): any {
|
||||
return key ? readState()?.[key] : readState()
|
||||
}
|
||||
|
||||
export async function setState(key: string, value: any) {
|
||||
const state = readState()
|
||||
state[key] = value
|
||||
await saveState(state)
|
||||
}
|
||||
|
||||
export async function clearState(key: string) {
|
||||
await setState(key, undefined)
|
||||
}
|
||||
|
||||
async function saveState(newState: any) {
|
||||
const unlock = await mutex.lock()
|
||||
try {
|
||||
await Bun.write(statePath, JSON.stringify(newState, null, 2))
|
||||
} finally {
|
||||
unlock()
|
||||
}
|
||||
}
|
||||
|
||||
function readState(): Record<string, any> {
|
||||
try {
|
||||
return JSON.parse(readFileSync(statePath, 'utf8'))
|
||||
} catch (e: any) {
|
||||
if (e.code === "ENOENT") return {}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user