NIP-83 ====== JavaScript Registry ------------------- `draft` `optional` JavaScript source files may be stored by relays, and then imported into web browsers or development environments. This NIP defines two kinds: - `8394` - JavaScript source file. - `8395` - TypeScript source file. In both cases, the `content` field contains the source code of the file. ```json { "kind": 8394, "id": "c17cf2f43580ad8238703ea32fb55c90c93402ca1f7d38085381f32c0f00e459", "pubkey": "c5dce01ee61fc62f62e2a825e2d598a839653b3175e0dcc072b1fe3c885f84a7", "created_at": 1709848074, "tags": [], "content": "/**\n * Infinite async generator. Iterates messages pushed to it until closed.\n * Only one consumer is expected to use a Machina instance at a time.\n *\n * @example\n * ```ts\n * // Create the Machina instance\n * const machina = new Machina();\n *\n * // Async generator loop\n * async function getMessages() {\n * for await (const msg of machina.stream()) {\n * console.log(msg);\n * }\n * }\n *\n * // Start the generator\n * getMessages();\n *\n * // Push messages to it\n * machina.push('hello!');\n * machina.push('whats up?');\n * machina.push('greetings');\n * ```\n */\nexport class Machina {\n #queue = [];\n #resolve;\n #aborted = false;\n\n constructor(signal) {\n if (signal?.aborted) {\n this.abort();\n } else {\n signal?.addEventListener('abort', () => this.abort(), { once: true });\n }\n }\n\n /** Get messages as an AsyncGenerator. */\n async *[Symbol.asyncIterator]() {\n while (!this.#aborted) {\n if (this.#queue.length) {\n yield this.#queue.shift();\n continue;\n }\n\n await new Promise((_resolve) => {\n this.#resolve = _resolve;\n });\n }\n\n throw new DOMException('The signal has been aborted', 'AbortError');\n }\n\n /** Push a message into the Machina instance, making it available to the consumer of `stream()`. */\n push(data) {\n this.#queue.push(data);\n this.#resolve?.();\n }\n\n /** Stops streaming and throws an error to the consumer. */\n abort() {\n this.#aborted = true;\n this.#resolve?.();\n }\n}\n", "sig": "f644529abdf7ea12c570800847ccc337201fe6c47ea8e09902017e04d6f87605c4c3ab7cece1189df9271a27df06e0da0c6f35375098004644d4dfbc38ba78e7" } ``` ```json { "kind": 8395, "id": "3047567edd9d74f694c648850fef128963973379be6a42f49d40c90524fbc079", "pubkey": "c5dce01ee61fc62f62e2a825e2d598a839653b3175e0dcc072b1fe3c885f84a7", "created_at": 1709847680, "tags": [], "content": "/**\n * Infinite async generator. Iterates messages pushed to it until closed.\n * Only one consumer is expected to use a Machina instance at a time.\n *\n * @example\n * ```ts\n * // Create the Machina instance\n * const machina = new Machina();\n *\n * // Async generator loop\n * async function getMessages() {\n * for await (const msg of machina.stream()) {\n * console.log(msg);\n * }\n * }\n *\n * // Start the generator\n * getMessages();\n *\n * // Push messages to it\n * machina.push('hello!');\n * machina.push('whats up?');\n * machina.push('greetings');\n * ```\n */\nexport class Machina {\n #queue: T[] = [];\n #resolve: (() => void) | undefined;\n #aborted = false;\n\n constructor(signal?: AbortSignal) {\n if (signal?.aborted) {\n this.abort();\n } else {\n signal?.addEventListener('abort', () => this.abort(), { once: true });\n }\n }\n\n /** Get messages as an AsyncGenerator. */\n async *[Symbol.asyncIterator](): AsyncGenerator {\n while (!this.#aborted) {\n if (this.#queue.length) {\n yield this.#queue.shift() as T;\n continue;\n }\n\n await new Promise((_resolve) => {\n this.#resolve = _resolve;\n });\n }\n\n throw new DOMException('The signal has been aborted', 'AbortError');\n }\n\n /** Push a message into the Machina instance, making it available to the consumer of `stream()`. */\n push(data: T): void {\n this.#queue.push(data);\n this.#resolve?.();\n }\n\n /** Stops streaming and throws an error to the consumer. */\n private abort(): void {\n this.#aborted = true;\n this.#resolve?.();\n }\n}\n", "sig": "b01ab6e2273bc33594dde5634c68add95c6273b8a864bf8f4c8a85564db3e3df2c9981a3c5d8694cc4fa0e0f984635d6b51d5ca352eff96b7cb789c39429d303" } ``` ## Immutability Source code events are considered immutable, and should NOT be deleted by supported clients or relays in response to kind `5` deletion requests. Relays may still remove content for any reason. Relays can indicate support for immutability by adding this NIP to their `supported_nips` field. ## Gateway It is possible to import JavaScript modules in supported runtimes using an HTTP Nostr gateway: ``` https:/// ``` A gateway will: - Try to look up the event (or else return 404). - Check that the event is of kind `8394` or `8395` (or else return 4xx). - Return the `content` field of the event as the response body. - Set an appropriate `Content-Type` header on the response. ### Usage with web browsers Web browsers can use script tags to import JavaScript modules from a gateway: ```html ``` It is also possible to use module imports within the script tag: ```html ``` See [JavaScript modules on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) for more details. ### Usage with Deno Like web browsers, Deno can import modules from a gateway using URLs: ```js import { Machina } from 'https://gateway.tld/note1xpr4vlkan460d9xxfzzslmcj393ewvmehe4y9ayagrys2f8mcpus9rk8kj'; ``` ### Import maps Import maps are supported by both [Deno](https://docs.deno.com/runtime/manual/basics/import_maps) and [web browsers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps), enabling us to configure the Nostr HTTP gateway once, and then use Nostr identifiers within our code: ```json { "imports": { "nostr/": "https://gateway.tld/" } } ``` Our code becomes: ```js import { Machina } from 'nostr/note1xpr4vlkan460d9xxfzzslmcj393ewvmehe4y9ayagrys2f8mcpus9rk8kj'; ``` Now if a gateway goes offline, we can switch to a different one by changing only the import map. ## JavaScript formats Kind `8394` events may be JavaScript modules (with import/export statements) or IIFE (immediately-invoked function expression) scripts. Kind `8395` events are TypeScript modules. ## Dependencies Modules may depend on other modules. In this case, import maps are NOT optional. All imports must either be absolute URLs, or they must use the `nostr/` prefix and be resolved using an import map. The user agent must resolve the import map before attempting to fetch the module.