NIP-83: JavaScript Registry

This commit is contained in:
Alex Gleason 2024-03-07 16:51:05 -06:00
parent 6871b3b334
commit 1158921a12
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7

120
83.md Normal file
View File

@ -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<string>();\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<string>();\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<T> {\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<T> {\n while (!this.#aborted) {\n if (this.#queue.length) {\n yield this.#queue.shift() as T;\n continue;\n }\n\n await new Promise<void>((_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://<host>/<nip19>
```
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
<script type="module" src="https://gateway.tld/note1xpr4vlkan460d9xxfzzslmcj393ewvmehe4y9ayagrys2f8mcpus9rk8kj"></script>
```
It is also possible to use module imports within the script tag:
```html
<script type="module">
import { Machina } from 'https://gateway.tld/note1xpr4vlkan460d9xxfzzslmcj393ewvmehe4y9ayagrys2f8mcpus9rk8kj';
</script>
```
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.