diff --git a/.gitignore b/.gitignore
index a14702c..ca1c303 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+data/
# dependencies (bun install)
node_modules
diff --git a/app/nose/bin/share.ts b/app/nose/bin/share.ts
index b99235f..e4b572f 100644
--- a/app/nose/bin/share.ts
+++ b/app/nose/bin/share.ts
@@ -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}: ${url}`
@@ -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: `${url}` }
}
\ No newline at end of file
diff --git a/app/nose/bin/state.ts b/app/nose/bin/state.ts
new file mode 100644
index 0000000..fe08919
--- /dev/null
+++ b/app/nose/bin/state.ts
@@ -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())
+}
\ No newline at end of file
diff --git a/app/src/config.ts b/app/src/config.ts
index 6f0b58f..60ba536 100644
--- a/app/src/config.ts
+++ b/app/src/config.ts
@@ -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()
\ No newline at end of file
diff --git a/app/src/mutex.ts b/app/src/mutex.ts
new file mode 100644
index 0000000..4269b2c
--- /dev/null
+++ b/app/src/mutex.ts
@@ -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)
+ }
+ })
+ }
+}
diff --git a/app/src/server.tsx b/app/src/server.tsx
index 6cd8ba3..abe8e0a 100644
--- a/app/src/server.tsx
+++ b/app/src/server.tsx
@@ -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",
diff --git a/app/src/shell.ts b/app/src/shell.ts
index 1f6bb05..97c550c 100644
--- a/app/src/shell.ts
+++ b/app/src/shell.ts
@@ -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
}
\ No newline at end of file
diff --git a/app/src/sneaker.ts b/app/src/sneaker.ts
index c617425..4b7557c 100644
--- a/app/src/sneaker.ts
+++ b/app/src/sneaker.ts
@@ -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 = {}
+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 {
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 {
+ try {
+ return JSON.parse(readFileSync(statePath, 'utf8'))
+ } catch (e: any) {
+ if (e.code === "ENOENT") return {}
+ throw e
+ }
+}