diff --git a/83.md b/83.md new file mode 100644 index 0000000000..9979dceffa --- /dev/null +++ b/83.md @@ -0,0 +1,120 @@ +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. \ No newline at end of file