From 948772165d44f7eef8b271f32e2f6b7c965fbea4 Mon Sep 17 00:00:00 2001 From: Reynaldi Chernando <12949382+reynaldichernando@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:29:46 +0700 Subject: [PATCH] Various docs fixes and AGENTS.md update (#4211) --- AGENTS.md | 47 +++- src/docs/src/AI/chat.md | 2 +- src/docs/src/AI/img2txt.md | 2 +- src/docs/src/AI/listModels.md | 2 +- src/docs/src/AI/speech2speech.md | 4 +- src/docs/src/AI/speech2txt.md | 2 +- src/docs/src/AI/txt2img.md | 32 +-- src/docs/src/AI/txt2vid.md | 4 +- src/docs/src/Apps/list.md | 2 +- src/docs/src/Auth/getProfile.md | 2 +- src/docs/src/Auth/getProfilePicture.md | 2 +- src/docs/src/Email.md | 4 +- src/docs/src/Events.md | 3 +- src/docs/src/Events/handlers.md | 2 +- src/docs/src/Events/onLocal.md | 214 +----------------- src/docs/src/Events/onPersistent.md | 4 +- src/docs/src/Events/subjects.md | 209 +++++++++++++++++ src/docs/src/FS.md | 3 +- src/docs/src/FS/listSharedByMe.md | 2 +- src/docs/src/FS/readdir.md | 2 +- src/docs/src/FS/space.md | 29 +-- src/docs/src/Hosting/list.md | 2 +- src/docs/src/KV.md | 4 +- src/docs/src/KV/list.md | 4 +- src/docs/src/KV/set.md | 2 +- src/docs/src/KV/update.md | 2 +- src/docs/src/Objects.md | 5 + src/docs/src/Peer.md | 1 + src/docs/src/Perms.md | 22 +- src/docs/src/Perms/request.md | 10 +- src/docs/src/UI/getLanguage.md | 2 +- src/docs/src/Workers/list.md | 2 +- src/docs/src/rate-limits-and-quotas.md | 4 +- .../deploy-workers-programmatically.md | 13 +- .../src/recipes/share-data-between-apps.md | 12 +- .../recipes/work-with-a-large-directory.md | 6 +- src/docs/src/sidebar.js | 47 +++- 37 files changed, 390 insertions(+), 321 deletions(-) create mode 100644 src/docs/src/Events/subjects.md diff --git a/AGENTS.md b/AGENTS.md index 6bcebfb5b..0ea415eb1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,7 +120,7 @@ Every SDK change carries all five of these, or the PR is incomplete. [doc/contri 1. **Backward compatibility**, unless a maintainer explicitly signs off on a break ([rules](doc/contributing-apis.md#maintaining-an-existing-api)). 2. **Tests** in the cross-platform API suite, or the Playwright harness for desktop-rendered `puter.ui.*` ([steps](doc/contributing-apis.md#6-tests)). -3. **Docs** in [src/docs/src/](src/docs/src/) that match the implementation exactly ([steps](doc/contributing-apis.md#5-docs)). +3. **Docs** in [src/docs/src/](src/docs/src/) that match the implementation exactly ([steps](doc/contributing-apis.md#5-docs), [writing rules](#developer-docs)). 4. **Types**, per the rules below. 5. **Error handling**: `{ message, code }` with stable `snake_case` codes, as in [modules/kv/](src/puter-js/src/modules/kv/) ([steps](doc/contributing-apis.md#3-puterjs)). @@ -146,6 +146,51 @@ Use `@typedef {Object}` + `@property` for any shape whose fields need documentin --- +## Developer docs + +[src/docs/src/](src/docs/src/) is the published puter.js documentation: one reference page per method (`/.md`, [steps](doc/contributing-apis.md#5-docs)) and recipes in [src/docs/src/recipes/](src/docs/src/recipes/). + +### Page structure + +The reference has two kinds of page: + +- **API overview pages** (`.md`, such as [KV.md](src/docs/src/KV.md)) give a quick top-level picture of what the API is for and what it can do, with short examples and a list of its functions that links every function page in the area. Leave out the details. The first sentence of the intro says what the API is about. This is a judgment call too: when the API depends on a Puter concept the reader may not know, such as Teams, the first sentence can name it and the sentences after it explain it. +- **Function pages** (`/.md`) hold the details: parameters, defaults, return values, errors, limits, and edge cases. + +An overview page answers "what is this API and is it what I need?", and a function page answers "how exactly does this call behave?". A change to one function usually only touches that function's page, so a new `kv.list()` option goes in `KV/list.md` without a matching update to `KV.md`. Don't copy a function's options, limits, or edge cases onto the overview page for completeness. Where a detail goes is a judgment call, not a hard rule: update the overview when a change affects how someone would describe the API as a whole. + +When you add a page, add it to [src/docs/src/sidebar.js](src/docs/src/sidebar.js) under its area, or it won't show up in the navigation. When you add a runnable example, add its HTML to `src/docs/src/playground/examples/.html`, register the slug in [src/docs/src/examples.js](src/docs/src/examples.js), and link the code block to it by putting the slug after the language with a semicolon (` ```html;kv-set `). + +### Writing docs + +These apply to reference pages and recipes alike. + +- **Document the developer-facing side only.** The reader is building on top of puter.js, so cover what affects their code: signatures, defaults, return shapes, errors, limits, and behavior they can observe. Leave out internal workings (which service handles the call, storage layout, caching, retries) unless they change what the developer has to do. [rate-limits-and-quotas.md](src/docs/src/rate-limits-and-quotas.md) is the exception: it publishes every limit, including timeouts and caps that come from how the backend works. +- **Describe the current state.** Docs aren't a changelog. Write how the API behaves now, without "now", "no longer", "existing code is unaffected", or other wording that compares it to an older version. +- **Verify before you document.** Run the calls you describe against a running Puter instead of inferring behavior from the source. +- **Write plainly.** Use the literal words a developer would say out loud, with contractions and everyday words. No idioms or figurative verbs ("walk away from something shared with you" → "remove your own access", "nothing to page through" → "without pagination"). No formal register ("present only while more pages remain" → "includes a `cursor` if there are more pages to load"). Prefer dev terms such as atomic, concurrent, and insertion order. +- **State the rule, then its consequence, in short sentences.** No metaphor in place of a rule. Name a concrete action ("you can create any files and folders inside it") over an abstract permission ("yours to organize freely"). Cut a sentence that only repeats the next one. +- **Describe behavior directly.** No quoted inner-voice phrases. Write "`isNew` is `true` when the person did not have access before", not "this tells 'I just gave this to someone' from 'they already had it'". +- **Keep references easy to follow.** Name what a list or code block belongs to in the sentence directly above it. Name a thing once and keep that name. Introduce an actor as "a user" before giving them a role: "A user can create a team. The user who creates the team becomes its owner." +- **Sentence mechanics.** Never start a sentence with inline code: write "To store data, use the [`set()`](/KV/set/) method." Link every inline mention of an API to its page, every time. No em dashes, and no colons or semicolons mid-sentence; a colon that introduces a code block is fine. +- **Show the correct way.** No "don't" and no wrong-way-first examples, in recipes and playground examples alike. Mention a wrong approach only when developers already hit it in practice, and state how it fails as a fact: "`add()` reads a plain object as a path map, so the call rejects with …". + +### Recipes + +Recipes are tested, copy-pasteable patterns for a task, one layer above the per-method reference, often combining several APIs. Agents find them through the `## Recipes` section of `llms.txt`. + +- **Files.** One `.md` per recipe in [src/docs/src/recipes/](src/docs/src/recipes/), with `title`, `description`, `tags`, and optional `order` front matter; they're picked up automatically. Tags must exist in [src/docs/src/recipe-tags.js](src/docs/src/recipe-tags.js) or the build fails. Quote a `description` that contains a colon followed by a space. Every top-level `.md` in that directory is built as a recipe, so keep other markdown out of it. +- **Title.** Short, Title Case, starts with a verb, and leaves out "to Your App" ("Add Team Support", "Deploy Workers Programmatically"). The slug is the title in kebab case, with no API-name prefix. In `llms.txt` an agent sees only the title and description before deciding to open a recipe, so word them to match the task the reader is trying to do. +- **Description.** Written for someone who has never heard of Puter. Start with "Learn how to", put the action first, and say what the reader's app gains: "Learn how to add file sharing to your app with Puter.js, so users can share files in their Puter account with other people." No API names, no mechanism, no list of example cases, and no "not just …" contrast. +- **Intro.** Explain the situation and why you'd do this, tied to something the reader already knows ("`puter.kv` is your database in Puter.js"). No "Use this when" bullet list. +- **Structure.** Get to the first working call as early as possible, then add validation, rate limits, and sign-in after it. Explain a Puter concept the first time it appears, such as what `me.puter` is or that `puter.workers.exec()` opens the Puter sign-in window. Split recipes by what the reader needs, not by technique, and give a family of recipes a shared tag. +- **Link to the reference for full option lists.** Mention only the options the recipe uses, then link to the function page for the rest ("All the sort options are listed on [`puter.fs.readdir()`](/FS/readdir/)."). A copied list of options, rules, or scopes goes stale when the reference changes. +- **Code.** Platform-neutral: just the code, with top-level `await`. No ` diff --git a/src/docs/src/Auth/getProfilePicture.md b/src/docs/src/Auth/getProfilePicture.md index 2c9f25942..9d32cf9f5 100644 --- a/src/docs/src/Auth/getProfilePicture.md +++ b/src/docs/src/Auth/getProfilePicture.md @@ -29,7 +29,7 @@ When signed out with no `username`, it resolves to `null` without opening a sign ## Example -```html;auth-get-profile-picture +```html diff --git a/src/docs/src/Email.md b/src/docs/src/Email.md index 3e07bfcc4..efe3e1bb3 100644 --- a/src/docs/src/Email.md +++ b/src/docs/src/Email.md @@ -6,9 +6,9 @@ platforms: [websites, apps, nodejs, workers] The Email API lets your app read the user's Puter mailbox and send transactional email. -Every Puter account has an email address, `{username}@puter.email`. Mail sent to it is stored in the user's own cloud drive, so your app can list the inbox and read messages, attachments included, once the user grants access to their mailbox. Your app can also send transactional email — a signup confirmation, a receipt, an alert — from a Puter-controlled address, with no mail server, sending domain, or DKIM to set up. A recipient with a Puter account can be reached at `{username}@puter.email`: that copy is filed straight into their Puter mailbox rather than relayed, on any plan, as long as they have set their mailbox up. +Every Puter account has an email address, `{username}@puter.email`. Mail sent to it is stored in the user's own cloud drive, so your app can list the inbox and read messages, attachments included, once the user grants access to their mailbox. Your app can also send transactional email, such as a signup confirmation, a receipt, or an alert, from a Puter address without setting up a mail server. -With the [User-Pays Model](/user-pays-model/), the account making the call covers its own usage: mail lives in the user's storage, and sends land on the caller's allowance rather than yours. +With the [User-Pays Model](/user-pays-model/), the account making the call covers its own usage. Mail is stored in the user's storage, and sends count toward the caller's allowance rather than yours.
Transactional email is available to accounts on a paid plan for now. A call from a free account fails with 402 subscription_required.
diff --git a/src/docs/src/Events.md b/src/docs/src/Events.md index 02b799f56..f1b67311c 100644 --- a/src/docs/src/Events.md +++ b/src/docs/src/Events.md @@ -22,7 +22,7 @@ What you watch is written as a subject string: | `kv:cart` | One key in your app's key-value store (`kv:cart*` for a prefix) | | `notif:account` | The user's notifications | -See [`onLocal()`](/Events/onLocal/#subjects) for the full subject syntax, the event shape, and how to share key-value events with another user. +See [Subjects and events](/Events/subjects/) for the full subject syntax, the event shape, and how to share key-value events with another user. With the [User-Pays Model](/user-pays-model/), deliveries are billed to the user holding the subscription, not to you. See [Rate Limits and Quotas](/rate-limits-and-quotas/#events) for limits and costs. @@ -270,6 +270,7 @@ These Events features are supported out of the box when using Puter.js: - **[`puter.events.fetch()`](/Events/fetch/)** - Read what a subject recorded while nothing was listening - **[`puter.events.handlers`](/Events/handlers/)** - Publish, list and remove the named handlers persistent subscriptions run - **[`puter.events.workers`](/Events/workers/)** - List and destroy an app's events worker +- **[Subjects and events](/Events/subjects/)** - The subject syntax and the shape of every event ## Examples diff --git a/src/docs/src/Events/handlers.md b/src/docs/src/Events/handlers.md index bfc122c16..023c247ff 100644 --- a/src/docs/src/Events/handlers.md +++ b/src/docs/src/Events/handlers.md @@ -104,7 +104,7 @@ An app's first published handler creates its [events worker](/Events/workers/), ### Refusing a delivery -In the events worker, a handler that returns takes the delivery, and one that throws is retried later. When retrying can't help (a malformed event, say), throw an error with `terminal: true` or `code: 'events_terminal'`. The delivery is dropped and replaced by a [gap marker](/Events/onLocal/#gaps) with `reason: 'handler_rejected'`. +In the events worker, a handler that returns takes the delivery, and one that throws is retried later. When retrying can't help (a malformed event, say), throw an error with `terminal: true` or `code: 'events_terminal'`. The delivery is dropped and replaced by a [gap marker](/Events/subjects/#gaps) with `reason: 'handler_rejected'`. ```js await puter.events.handlers.publish('ingestUpload', async ({ event }) => { diff --git a/src/docs/src/Events/onLocal.md b/src/docs/src/Events/onLocal.md index 1ad7e4a8f..af1f18255 100644 --- a/src/docs/src/Events/onLocal.md +++ b/src/docs/src/Events/onLocal.md @@ -6,7 +6,7 @@ platforms: [websites, apps, nodejs]
The Events API is in beta. Event shapes, limits, and behavior may change between releases.
-Subscribes to a subject and calls `handler` each time something matching it changes. The subscription belongs to this client's connection: nothing is stored, nothing runs while the page is closed, and it ends when the connection does. Changes from other devices and regions reach it too; one made in another region typically arrives a few hundred milliseconds later than a local one. See [Events](/Events/) for the subject grammar and the event shape. +Subscribes to a subject and calls `handler` each time something matching it changes. The subscription belongs to this client's connection: nothing is stored, nothing runs while the page is closed, and it ends when the connection does. Changes from other devices and regions reach it too; one made in another region typically arrives a few hundred milliseconds later than a local one. See [Subjects and events](/Events/subjects/) for the subject syntax and the event shape. Not for Puter workers: a subscription there only lasts for one invocation. To react to changes from a worker, use [`onPersistent()`](/Events/onPersistent/) with a published handler. @@ -21,16 +21,16 @@ puter.events.onLocal(subject, handler, options) ## Parameters #### `subject` (String) (required) -What to watch: `fs:[:]`, `kv:`, or `notif:`. See [Subjects](#subjects). +What to watch: `fs:[:]`, `kv:`, or `notif:`. See [Subjects](/Events/subjects/). #### `handler` (Function) (required) -Called with `{ event }` for each delivery; see [The event](#the-event). `event.op === 'gap'` means events were dropped by a limit or missed while the connection was down: re-read what you're watching (see [Gaps](#gaps)). A handler that throws is logged to the console; the subscription continues. +Called with `{ event }` for each delivery; see [The event](/Events/subjects/#the-event). `event.op === 'gap'` means events were dropped by a limit or missed while the connection was down: re-read what you're watching (see [Gaps](/Events/subjects/#gaps)). A handler that throws is logged to the console; the subscription continues. #### `options` (Object) (optional) - `onError` (Function): Called with `{ message, code }` if the subscription can't be restored after a disconnect: re-subscribing failed, the reconnect was refused, or the server kept closing the connection. A single disconnect is reconnected without calling it; the handler gets a `reconnect` gap instead. After `onError`, the subscription is over; call `onLocal()` again to resume. It's also called with `code: 'subscription_ended'` and a `reason` when the server ends the subscription (see [Subscriptions the server ends](#subscriptions-the-server-ends)). Without it, the error is logged to the console. - `timeout` (Number): How long to wait for the server to confirm the subscription, in milliseconds. Defaults to `30000`. -- `includeValue` (Boolean): For a `kv:` subject, include the key's new value as `event.value` (`null` on a `del`, absent on an `expire`). See [Getting the new value](#getting-the-new-value) for when it's left out. Refused on other subjects. +- `includeValue` (Boolean): For a `kv:` subject, include the key's new value as `event.value` (`null` on a `del`, absent on an `expire`). See [Getting the new value](/Events/subjects/#getting-the-new-value) for when it's left out. Refused on other subjects. ## Return value @@ -66,215 +66,13 @@ The promise rejects with `{ message, code }`: | `events_connection_failed` | The connection couldn't be opened, the server didn't answer in time, or it kept closing the connection. | | `events_failed` | The server sent a response the SDK couldn't read. | -## Subjects - -``` -fs:[:] -kv: -kv:: -notif: -notif:: -``` - -The same subjects work with [`onPersistent()`](/Events/onPersistent/). - -### Files - -- **Path**: absolute (`/alice/Documents`), home-relative (`~/Documents`), or relative (`inbox` or `./inbox`). A relative path resolves the same way [`puter.fs`](/FS/) paths do: under your app's `AppData` folder in an app, under home otherwise. A directory covers everything under it, at any depth. -- **Uid**: the `uid` of a file or directory, to follow one node wherever it moves. A relative name shaped like a uid is read as one; write `./` to mean the path. -- **Op**: one of `add`, `write`, `move`, `remove`, `meta`. Leave it off to get all of them. Nothing emits `meta` yet. - -```js -await puter.events.onLocal('fs:~/Documents', handler); // everything under Documents -await puter.events.onLocal('fs:inbox', handler); // inbox in your app's folder -await puter.events.onLocal('fs:~/Documents/notes.txt:write', handler); // one file, writes only -await puter.events.onLocal('fs:~/Pictures/*.png', handler); // wildcard within one segment -await puter.events.onLocal('fs:~/Projects/**/build.log', handler); // across directories -``` - -`*` matches within one path segment, `**` crosses directories, and `?` matches one character. A subject may use one `*` per segment and one `**` in total; more is rejected with `invalid_subject_pattern`. - -#### Watching a path before it exists - -A subject can name a path that isn't there. The subscription anchors on the nearest existing directory and matches the rest of the path, so it behaves as if the path already existed: you get the event when it appears and, for a folder, every change inside it afterwards. A pattern still matches only the paths it names. - -```js -// Nothing at this path yet. The handler runs when it's created. -await puter.events.onLocal('fs:~/Documents/inbox/trigger.json:add', ({ event }) => { - process(event.path); -}); -``` - -If the node a subscription is attached to is deleted, a subscription made with a uid, or with a path that existed when you subscribed, ends: `onError` is called with `subscription_ended` and `reason: 'anchor_deleted'`. Call `onLocal()` again to watch the path once it's back. One made with a pattern, or with a path that didn't exist yet, moves up to the nearest folder that still exists and that you can read, and keeps watching, so recreating the path resumes delivery. If there's no such folder (for example, the owner of a folder shared with you deleted it), or the pattern would grow past its limits, it ends the same way. - -### Notifications - -`notif:` watches part of the account's notification mailbox: - -- `notif:account`: notifications about the account itself, such as files shared with it. Only a client acting as the account, not as an app, sees them. Apps and websites never do: subscribing from one is refused with `subject_does_not_exist`. -- `notif:app-user`: notifications belonging to the app you're running as. -- `notif:developer`: notifications about an app you own. - -The two-segment form is expanded to your app's id for you. Notifications are stored, so you can also read missed ones with [`fetch()`](/Events/fetch/). - -### Key-value data - -A `kv:` subject watches your app's key-value store: - -```js -await puter.events.onLocal('kv:cart', ({ event }) => refresh(event.key)); // exactly the key `cart` -await puter.events.onLocal('kv:cart*', handler); // every key starting with `cart` -``` - -> **Exact by default; add `*` to widen.** `kv:cart` matches only `cart`, and `kv:cart*` matches every key starting with `cart`. This is the opposite of [`puter.kv.list()`](/KV/list/), which always matches a prefix. - -Only a trailing `*` is allowed. A `*` anywhere else, or a `?`, is rejected with `invalid_kv_pattern`. - -The segment after `kv:` is read as an app id when there are more segments after it, so a key that contains `:` needs the full three-part form: - -```js -await puter.events.onLocal('kv:orders:pending', handler); // app `orders`, key `pending` -await puter.events.onLocal(`kv:${puter.appID}:orders:pending`, handler); // your app, key `orders:pending` -``` - -The returned `anchor` names the app whose store is watched; `subject` stays as you passed it. - -#### Getting the new value - -A delivery names the key, not its value. Pass `includeValue: true` to receive the new value as `event.value` (`null` on a `del`, absent on an `expire`): - -```js -await puter.events.onLocal('kv:cart', ({ event }) => render(event.value), { includeValue: true }); -``` - -The value is left out, and you read the key yourself, when: - -- it's over **16 KB** serialized, or -- more than **128** subscriptions match the change in that region (including ones that didn't ask for values), or the filter-check limit stops the count early. - -#### Data from other apps - -Watching another app's key-value data takes the same consent as reading it: the app must allow data sharing, and the user must grant your app `app-data::kv:read`. Both are checked when you subscribe and on every delivery. Where cross-app watching is off, the subject is refused with `events_cross_app_disabled`. - -Entries the other app wrote with `disableSharing` are never delivered. Marking a key private produces no event, so a value you already have for it may be stale. - -## Sharing key-value events with another user - -`kv:` always means your own data. To let another account watch part of yours, mint a **share handle** over a key prefix and give it to them. They subscribe with the handle where an app id would go: - -```js -// The owner shares one workspace with another account. -const res = await fetch(`${puter.APIOrigin}/events/kv-handles`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${puter.authToken}`, - }, - body: JSON.stringify({ - granteeUsername: 'bob', - // Use a segment you'll never rename: the handle is fixed to this prefix. - prefix: `workspace:${workspaceId}:`, - }), -}); -const { handle } = await res.json(); -``` - -```js -// Bob watches every key written in that workspace. -// `event.key` is relative to the handle: `messages:1`, not `workspace::messages:1`. -await puter.events.onLocal(`kv:${handle}:*`, ({ event }) => render(event.key)); -``` - -- **The holder learns only the handle**: not whose data it is, where it sits, or anything above the prefix. `subject` and `key` in their deliveries are relative to the handle. `kv::messages:*` narrows to part of the shared region. -- **Values:** `includeValue` works through a handle, with the same limits. A handle grants events, not reads, so when a value is left out the holder needs some other authorized way to read it. -- **Private entries stay private.** An entry written with `disableSharing` is never delivered through a handle: no event, key, or value. -- **Use a stable prefix.** A handle stays on the prefix it was minted on. Rename `workspace::` to `project::` and existing handles point at keys nothing writes. Prefer synthetic ids (`workspace::`, `thread::`) over names that might change (`acme-corp:`). -- **Prefixes are literal.** `*`, `?` and empty segments (`workspace::abc:`) are refused with `invalid_kv_share_prefix`. The trailing `:` is optional. -- **Keys can't leave the handle.** A handle with no key after it, or a key using `..`, is refused with `invalid_kv_handle_key`. - -**Managing handles.** `GET /events/kv-handles` lists the handles the account has minted, revoked ones included. `DELETE /events/kv-handles/` revokes one, and every persistent subscription on it is suspended with `permission_revoked` and its backlog dropped. Revoking an already-revoked handle isn't an error. See [Rate Limits and Quotas](/rate-limits-and-quotas/#events) for how many handles an account can hold. Temporary accounts can't mint handles (`events_kv_handle_requires_account`). Where the feature is off, minting and handle subjects fail with `events_kv_handles_disabled`. - -**Apps.** An app can mint, list and revoke handles for its user, but only inside its own namespace and with the user's consent: `manage:kv-share:::`, requested with [`puter.perms.request()`](/Perms/request/). The prefix supplies its segments, so `workspace:abc:` ends the permission as `…:workspace:abc`. The consent must name a prefix; asking for the whole namespace is refused with `invalid_kv_share_prefix`. An app that lists handles sees only its own namespace. - -| Refused with | When | -| --- | --- | -| `events_kv_handle_not_delegated` | Minting or revoking outside the consented prefix, or after the consent was withdrawn (the handle stays in place). | -| `events_kv_handle_outside_namespace` | Minting outside the app's own namespace. | -| `events_kv_handle_owner_only` | The caller is an access token the app issued, a different app of the same user, or the handle is outside the app's namespace. | - -An app can also subscribe through a handle, but only when the shared region belongs to that same app. A different app, even for the same user, is refused as if the handle didn't exist. - -## Access - -Subscribing takes the same access as reading. A subject you can't read and a subject that doesn't exist both fail with `subject_does_not_exist`, so the error doesn't reveal which. Access is checked again on every delivery: when a share is revoked, deliveries stop immediately. The subscription itself stays open, so `onError` isn't called — not even if the node is later deleted. - -## The event - -The handler is called with `{ event }`. A filesystem change carries: - -| Field | Type | Description | -| --- | --- | --- | -| `id` | String | Unique id for the event. | -| `subject` | String | The node it happened to, as `fs::`. Not the subject you subscribed with. | -| `op` | String | `add` (a create, or a copy's destination), `write`, `move` (a move or a rename), or `remove`. | -| `uid` | String | The uid of the node that changed. | -| `path` | String | The path of the node that changed. | -| `from` | String | On a `move`, the path the node left. Only present when the subscription was watching that location. | -| `self` | Boolean | `true` when the account holding the subscription made the change. Use it to ignore your own writes. | -| `ts` | Number | When it happened, in milliseconds since the epoch. | -| `seq` | Number | Position within one dispatch, when a change goes to several subscriptions. | - -A key-value change carries `key` instead of `uid` and `path`, and different ops: - -| Field | Type | Description | -| --- | --- | --- | -| `id` | String | Unique id for the event. | -| `subject` | String | `kv::`, naming the key that changed. | -| `op` | String | `set` for a write, `del` for a removal, `expire` when only the key's lifetime changed. | -| `key` | String | The key that changed. | -| `value` | Any | The new value (`null` on a `del`). Only with `includeValue`, and only within the [limits above](#getting-the-new-value). Never on an `expire`. | -| `self` | Boolean | As above. | -| `ts` | Number | As above. | -| `seq` | Number | As above. | - -A notification carries the fields listed on [`fetch()`](/Events/fetch/#return-value). - -Events never say *who* made a change: on a shared folder, that would tell every subscriber who else has access. - -[`puter.kv.flush()`](/KV/flush/) delivers nothing, since the keys a flush can list aren't reliably the keys it removed. - -Writes to the same subject within 250 ms arrive as one event carrying the latest state, so a multipart upload or a save loop is one delivery rather than one per write. - -### Gaps - -Limits never make a subscription fail. Instead they send a **gap marker** in place of what was dropped: an event with `op: 'gap'`, a `reason`, and no `uid` or `path`. Treat it as "re-read what I'm watching", never as "nothing changed". A reconnect sends one too. - -| `reason` | Cause | -| --- | --- | -| `matched_subscription_limit` | The event matched more subscriptions than one event may reach. | -| `filter_evaluation_limit` | The event had more subscription filters to check than one event may check. | -| `delivery_rate_limit` | This subscription went over its per-minute delivery rate. | -| `backlog_overflow` | A persistent subscription's undelivered backlog was full, so the oldest deliveries were dropped. | -| `handler_rejected` | A persistent subscription's handler refused the delivery (a `4xx`, or a thrown terminal error). | -| `suspended_backlog_expired` | A persistent subscription stayed [suspended](/Events/onPersistent/#suspended-subscriptions) past the time its backlog is kept. | -| `reconnect` | The connection dropped and came back. Changes made while it was down weren't delivered. Sent once, after the subscription is restored. | - -```js -await puter.events.onLocal('fs:~/Documents', async ({ event }) => { - if (event.op === 'gap') return refreshEverything(); - apply(event); -}); -``` - -See [Rate Limits and Quotas](/rate-limits-and-quotas/#events) for the numbers behind each limit. - ## Reconnects All of a client's session subscriptions share one connection, opened by the first `onLocal()` and closed when the last one ends. If the connection drops (a network blip, a sign-in, an API origin change, or the server closing it), the SDK reconnects and subscribes again. The handler and subscription object stay the same; only `subId` changes, so don't store anything against it. `onError` is called only if the subscription can't be restored: re-subscribing fails, the reconnect is refused (`reauth_required` after a sign-out), or the server keeps closing the connection (`events_connection_failed`). -Changes made while it was down aren't sent afterwards. Once the subscription is back, the handler gets one [gap marker](#gaps) with `reason: 'reconnect'`, so a handler that re-reads on gaps catches up by itself. For `notif:` subjects, [`fetch()`](/Events/fetch/) reads exactly what was missed. +Changes made while it was down aren't sent afterwards. Once the subscription is back, the handler gets one [gap marker](/Events/subjects/#gaps) with `reason: 'reconnect'`, so a handler that re-reads on gaps catches up by itself. For `notif:` subjects, [`fetch()`](/Events/fetch/) reads exactly what was missed. ```js const sub = await puter.events.onLocal('fs:~/Documents', handler, { @@ -288,7 +86,7 @@ The server can end a session subscription on its own, without a disconnect. `onE | `code` | `reason` | When | | --- | --- | --- | -| `subscription_ended` | `anchor_deleted` | The node it was attached to was deleted, and it couldn't move up to a parent folder (see [Watching a path before it exists](#watching-a-path-before-it-exists)). | +| `subscription_ended` | `anchor_deleted` | The node it was attached to was deleted, and it couldn't move up to a parent folder (see [Watching a path before it exists](/Events/subjects/#watching-a-path-before-it-exists)). | If the connection is down when this happens, or drops before `onError` is called, the SDK isn't told. It subscribes again with the original subject when it reconnects: a path then anchors on its nearest existing folder and keeps watching, like a path that doesn't exist yet, while a uid fails and `onError` is called with `subject_does_not_exist` instead. diff --git a/src/docs/src/Events/onPersistent.md b/src/docs/src/Events/onPersistent.md index 520f36279..61bffaa95 100644 --- a/src/docs/src/Events/onPersistent.md +++ b/src/docs/src/Events/onPersistent.md @@ -10,7 +10,7 @@ Creates a subscription that outlives this connection. It's stored on the account The subscription is live immediately in the region where it was created. Changes made in other regions can take a moment longer (usually well under a second) to reach it right after this call resolves. -See [`onLocal()`](/Events/onLocal/#subjects) for the subject grammar and the event shape. +See [Subjects and events](/Events/subjects/) for the subject syntax and the event shape. On a website with nobody signed in, it asks the user to sign in first, as other Puter.js calls do. An app running on Puter is always signed in. @@ -59,7 +59,7 @@ Whichever place runs it, it's the same code, called with: | Binding | What it is | | --- | --- | -| `event` | The [event](/Events/onLocal/#the-event), or a [gap marker](/Events/onLocal/#gaps). | +| `event` | The [event](/Events/subjects/#the-event), or a [gap marker](/Events/subjects/#gaps). | | `ctx` | The frozen `context` this subscription was created with. | | `user` | A `puter` bound to the account holding the subscription, with the same access your app has for that user in a tab. In a client, it's the ambient `puter`. | | `fetch` | [`puter.net.fetch`](/Networking/fetch/) where it exists, otherwise the environment's `fetch`. | diff --git a/src/docs/src/Events/subjects.md b/src/docs/src/Events/subjects.md new file mode 100644 index 000000000..25900ba99 --- /dev/null +++ b/src/docs/src/Events/subjects.md @@ -0,0 +1,209 @@ +--- +title: Subjects and events +description: The subject strings Events subscriptions watch, and the events they deliver. +platforms: [websites, apps, nodejs, workers] +--- + +
The Events API is in beta. Event shapes, limits, and behavior may change between releases.
+ +A subject is the string that says what a subscription watches. [`onLocal()`](/Events/onLocal/) and [`onPersistent()`](/Events/onPersistent/) take the same subjects and deliver the same events. + +## Syntax + +``` +fs:[:] +kv: +kv:: +notif: +notif:: +``` + +## Files + +- **Path**: absolute (`/alice/Documents`), home-relative (`~/Documents`), or relative (`inbox` or `./inbox`). A relative path resolves the same way [`puter.fs`](/FS/) paths do: under your app's `AppData` folder in an app, under home otherwise. A directory covers everything under it, at any depth. +- **Uid**: the `uid` of a file or directory, to follow one node wherever it moves. A relative name shaped like a uid is read as one; write `./` to mean the path. +- **Op**: one of `add`, `write`, `move`, `remove`, `meta`. Leave it off to get all of them. Nothing emits `meta` yet. + +```js +await puter.events.onLocal('fs:~/Documents', handler); // everything under Documents +await puter.events.onLocal('fs:inbox', handler); // inbox in your app's folder +await puter.events.onLocal('fs:~/Documents/notes.txt:write', handler); // one file, writes only +await puter.events.onLocal('fs:~/Pictures/*.png', handler); // wildcard within one segment +await puter.events.onLocal('fs:~/Projects/**/build.log', handler); // across directories +``` + +`*` matches within one path segment, `**` crosses directories, and `?` matches one character. A subject may use one `*` per segment and one `**` in total; more is rejected with `invalid_subject_pattern`. + +### Watching a path before it exists + +A subject can name a path that isn't there. The subscription anchors on the nearest existing directory and matches the rest of the path, so it behaves as if the path already existed: you get the event when it appears and, for a folder, every change inside it afterwards. A pattern still matches only the paths it names. + +```js +// Nothing at this path yet. The handler runs when it's created. +await puter.events.onLocal('fs:~/Documents/inbox/trigger.json:add', ({ event }) => { + process(event.path); +}); +``` + +If the node a subscription is attached to is deleted, a subscription made with a uid, or with a path that existed when you subscribed, ends with `reason: 'anchor_deleted'`. Subscribe again to watch the path once it's back. [`onLocal()`](/Events/onLocal/#subscriptions-the-server-ends) reports this through `onError`, and a persistent subscription reports it with an `app.events.ended` notification you can read with [`fetch()`](/Events/fetch/). One made with a pattern, or with a path that didn't exist yet, moves up to the nearest folder that still exists and that you can read, and keeps watching, so recreating the path resumes delivery. If there's no such folder (for example, the owner of a folder shared with you deleted it), or the pattern would grow past its limits, it ends the same way. + +## Notifications + +`notif:` watches part of the account's notification mailbox: + +- `notif:account`: notifications about the account itself, such as files shared with it. Only a client acting as the account, not as an app, sees them. Apps and websites never do: subscribing from one is refused with `subject_does_not_exist`. +- `notif:app-user`: notifications belonging to the app you're running as. +- `notif:developer`: notifications about an app you own. + +The two-segment form is expanded to your app's id for you. Notifications are stored, so you can also read missed ones with [`fetch()`](/Events/fetch/). + +## Key-value data + +A `kv:` subject watches your app's key-value store: + +```js +await puter.events.onLocal('kv:cart', ({ event }) => refresh(event.key)); // exactly the key `cart` +await puter.events.onLocal('kv:cart*', handler); // every key starting with `cart` +``` + +> **Exact by default; add `*` to widen.** `kv:cart` matches only `cart`, and `kv:cart*` matches every key starting with `cart`. This is the opposite of [`puter.kv.list()`](/KV/list/), which always matches a prefix. + +Only a trailing `*` is allowed. A `*` anywhere else, or a `?`, is rejected with `invalid_kv_pattern`. + +The segment after `kv:` is read as an app id when there are more segments after it, so a key that contains `:` needs the full three-part form: + +```js +await puter.events.onLocal('kv:orders:pending', handler); // app `orders`, key `pending` +await puter.events.onLocal(`kv:${puter.appID}:orders:pending`, handler); // your app, key `orders:pending` +``` + +The returned `anchor` names the app whose store is watched; `subject` stays as you passed it. + +### Getting the new value + +A delivery names the key, not its value. Pass `includeValue: true` to receive the new value as `event.value` (`null` on a `del`, absent on an `expire`): + +```js +await puter.events.onLocal('kv:cart', ({ event }) => render(event.value), { includeValue: true }); +``` + +The value is left out, and you read the key yourself, when: + +- it's over **16 KB** serialized, or +- more than **128** subscriptions match the change in that region (including ones that didn't ask for values), or the filter-check limit stops the count early. + +### Data from other apps + +Watching another app's key-value data takes the same consent as reading it: the app must allow data sharing, and the user must grant your app `app-data::kv:read`. Both are checked when you subscribe and on every delivery. Where cross-app watching is off, the subject is refused with `events_cross_app_disabled`. + +Entries the other app wrote with `disableSharing` are never delivered. Marking a key private produces no event, so a value you already have for it may be stale. + +## Sharing key-value events with another user + +`kv:` always means your own data. To let another account watch part of yours, mint a **share handle** over a key prefix and give it to them. They subscribe with the handle where an app id would go: + +```js +// The owner shares one workspace with another account. +const res = await fetch(`${puter.APIOrigin}/events/kv-handles`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${puter.authToken}`, + }, + body: JSON.stringify({ + granteeUsername: 'bob', + // Use a segment you'll never rename: the handle is fixed to this prefix. + prefix: `workspace:${workspaceId}:`, + }), +}); +const { handle } = await res.json(); +``` + +```js +// Bob watches every key written in that workspace. +// `event.key` is relative to the handle: `messages:1`, not `workspace::messages:1`. +await puter.events.onLocal(`kv:${handle}:*`, ({ event }) => render(event.key)); +``` + +- **The holder learns only the handle**: not whose data it is, where it sits, or anything above the prefix. `subject` and `key` in their deliveries are relative to the handle. `kv::messages:*` narrows to part of the shared region. +- **Values:** `includeValue` works through a handle, with the same limits. A handle grants events, not reads, so when a value is left out the holder needs some other authorized way to read it. +- **Private entries stay private.** An entry written with `disableSharing` is never delivered through a handle: no event, key, or value. +- **Use a stable prefix.** A handle stays on the prefix it was minted on. Rename `workspace::` to `project::` and existing handles point at keys nothing writes. Prefer synthetic ids (`workspace::`, `thread::`) over names that might change (`acme-corp:`). +- **Prefixes are literal.** `*`, `?` and empty segments (`workspace::abc:`) are refused with `invalid_kv_share_prefix`. The trailing `:` is optional. +- **Keys can't leave the handle.** A handle with no key after it, or a key using `..`, is refused with `invalid_kv_handle_key`. + +**Managing handles.** `GET /events/kv-handles` lists the handles the account has minted, revoked ones included. `DELETE /events/kv-handles/` revokes one, and every persistent subscription on it is suspended with `permission_revoked` and its backlog dropped. Revoking an already-revoked handle isn't an error. See [Rate Limits and Quotas](/rate-limits-and-quotas/#events) for how many handles an account can hold. Temporary accounts can't mint handles (`events_kv_handle_requires_account`). Where the feature is off, minting and handle subjects fail with `events_kv_handles_disabled`. + +**Apps.** An app can mint, list and revoke handles for its user, but only inside its own namespace and with the user's consent: `manage:kv-share:::`, requested with [`puter.perms.request()`](/Perms/request/). The prefix supplies its segments, so `workspace:abc:` ends the permission as `…:workspace:abc`. The consent must name a prefix; asking for the whole namespace is refused with `invalid_kv_share_prefix`. An app that lists handles sees only its own namespace. + +| Refused with | When | +| --- | --- | +| `events_kv_handle_not_delegated` | Minting or revoking outside the consented prefix, or after the consent was withdrawn (the handle stays in place). | +| `events_kv_handle_outside_namespace` | Minting outside the app's own namespace. | +| `events_kv_handle_owner_only` | The caller is an access token the app issued, a different app of the same user, or the handle is outside the app's namespace. | + +An app can also subscribe through a handle, but only when the shared region belongs to that same app. A different app, even for the same user, is refused as if the handle didn't exist. + +## Access + +Subscribing takes the same access as reading. A subject you can't read and a subject that doesn't exist both fail with `subject_does_not_exist`, so the error doesn't reveal which. Access is checked again on every delivery: when a share is revoked, deliveries stop immediately. The subscription itself stays open, even if the node is later deleted. + +## The event + +The handler is called with `{ event }`. A filesystem change carries: + +| Field | Type | Description | +| --- | --- | --- | +| `id` | String | Unique id for the event. | +| `subject` | String | The node it happened to, as `fs::`. Not the subject you subscribed with. | +| `op` | String | `add` (a create, or a copy's destination), `write`, `move` (a move or a rename), or `remove`. | +| `uid` | String | The uid of the node that changed. | +| `path` | String | The path of the node that changed. | +| `from` | String | On a `move`, the path the node left. Only present when the subscription was watching that location. | +| `self` | Boolean | `true` when the account holding the subscription made the change. Use it to ignore your own writes. | +| `ts` | Number | When it happened, in milliseconds since the epoch. | +| `seq` | Number | Position within one dispatch, when a change goes to several subscriptions. | + +A key-value change carries `key` instead of `uid` and `path`, and different ops: + +| Field | Type | Description | +| --- | --- | --- | +| `id` | String | Unique id for the event. | +| `subject` | String | `kv::`, naming the key that changed. | +| `op` | String | `set` for a write, `del` for a removal, `expire` when only the key's lifetime changed. | +| `key` | String | The key that changed. | +| `value` | Any | The new value (`null` on a `del`). Only with `includeValue`, and only within the [limits above](#getting-the-new-value). Never on an `expire`. | +| `self` | Boolean | As above. | +| `ts` | Number | As above. | +| `seq` | Number | As above. | + +A notification carries the fields listed on [`fetch()`](/Events/fetch/#return-value). + +Events never say *who* made a change: on a shared folder, that would tell every subscriber who else has access. + +[`puter.kv.flush()`](/KV/flush/) delivers nothing, since the keys a flush can list aren't reliably the keys it removed. + +Writes to the same subject within 250 ms arrive as one event carrying the latest state, so a multipart upload or a save loop is one delivery rather than one per write. + +### Gaps + +Limits never make a subscription fail. Instead they send a **gap marker** in place of what was dropped: an event with `op: 'gap'`, a `reason`, and no `uid` or `path`. Treat it as "re-read what I'm watching", never as "nothing changed". A reconnect sends one too. + +| `reason` | Cause | +| --- | --- | +| `matched_subscription_limit` | The event matched more subscriptions than one event may reach. | +| `filter_evaluation_limit` | The event had more subscription filters to check than one event may check. | +| `delivery_rate_limit` | This subscription went over its per-minute delivery rate. | +| `backlog_overflow` | A persistent subscription's undelivered backlog was full, so the oldest deliveries were dropped. | +| `handler_rejected` | A persistent subscription's handler refused the delivery (a `4xx`, or a thrown terminal error). | +| `suspended_backlog_expired` | A persistent subscription stayed [suspended](/Events/onPersistent/#suspended-subscriptions) past the time its backlog is kept. | +| `reconnect` | The connection dropped and came back. Changes made while it was down weren't delivered. Sent once, after the subscription is restored. | + +```js +await puter.events.onLocal('fs:~/Documents', async ({ event }) => { + if (event.op === 'gap') return refreshEverything(); + apply(event); +}); +``` + +See [Rate Limits and Quotas](/rate-limits-and-quotas/#events) for the numbers behind each limit. diff --git a/src/docs/src/FS.md b/src/docs/src/FS.md index bc67a5140..627b41053 100644 --- a/src/docs/src/FS.md +++ b/src/docs/src/FS.md @@ -5,8 +5,6 @@ description: Store and manage data in the user's own cloud drive with Puter.js f The Cloud Storage API lets you store and manage data in the cloud. -Local [uploads](/FS/upload/) can optionally generate browser image thumbnails or use a custom thumbnail callback. The callback can delegate to the built-in image generator and respond to upload cancellation. The Puter desktop additionally provides PDF previews without adding a PDF renderer to the SDK. - It comes with a comprehensive but familiar file system operations including write, read, delete, move, and copy for files, plus powerful directory management features like creating directories, listing contents, and much more. With Puter.js, you don't need to worry about setting up storage infrastructure such as configuring buckets, managing CDNs, or ensuring availability, since everything is handled for you. Additionally, with the [User-Pays Model](/user-pays-model/), you don't have to worry about storage or bandwidth costs, as users of your application cover their own usage. @@ -306,6 +304,7 @@ These cloud storage features are supported out of the box when using Puter.js: - **[`puter.fs.copy()`](/FS/copy/)** - Copy a file or directory - **[`puter.fs.move()`](/FS/move/)** - Move a file or directory - **[`puter.fs.stat()`](/FS/stat/)** - Get information about a file or directory +- **[`puter.fs.space()`](/FS/space/)** - Get the user's storage capacity and usage - **[`puter.fs.delete()`](/FS/delete/)** - Delete a file or directory - **[`puter.fs.upload()`](/FS/upload/)** - Upload a file from the local system - **[`puter.fs.getReadURL()`](/FS/getReadURL/)** - Generate a URL that can be used to read a file diff --git a/src/docs/src/FS/listSharedByMe.md b/src/docs/src/FS/listSharedByMe.md index f9709746d..0acecd6df 100644 --- a/src/docs/src/FS/listSharedByMe.md +++ b/src/docs/src/FS/listSharedByMe.md @@ -45,7 +45,7 @@ Items you own appear at their real path. An item you shared as a delegate (from See everything you have shared -```html;fs-listSharedByMe +```html diff --git a/src/docs/src/FS/readdir.md b/src/docs/src/FS/readdir.md index 543566d95..7335906cf 100755 --- a/src/docs/src/FS/readdir.md +++ b/src/docs/src/FS/readdir.md @@ -49,7 +49,7 @@ When the request includes `cursor` (even `null`) or `includeTotal`, or is `recur - `cursor` (String) (optional): Present while more pages exist; pass it to the next call. - `total` (Number) (optional): Total entry count, present when `includeTotal` was set. -Requests without pagination params keep returning the full listing as a plain array, so existing code is unaffected — under the hood the SDK now fetches it page by page. +Without pagination params, the full listing is returned as a plain array. With `stream: true`, the method returns an async iterator of page objects instead: diff --git a/src/docs/src/FS/space.md b/src/docs/src/FS/space.md index 0fdd0aaa9..9f02e6fc1 100755 --- a/src/docs/src/FS/space.md +++ b/src/docs/src/FS/space.md @@ -1,40 +1,43 @@ --- title: puter.fs.space() description: Check how much storage capacity the user has and how much they have used. +platforms: [websites, apps, nodejs, workers] --- -Returns the storage space capacity and usage for the current user. - -
- -This method requires permission to access the user's storage space. If the user has not granted permission, the method will return an error.
+Returns the storage capacity and usage of the current user. ## Syntax + ```js puter.fs.space() ``` ## Parameters + None. ## Return value -A `Promise` that will resolve to an object with the following properties: -- `capacity` (Number): The total amount of storage capacity available to the user, in bytes. -- `used` (Number): The amount of storage space used by the user, in bytes. +A `Promise` that resolves to a [`SpaceInfo`](/Objects/spaceinfo/) object with these properties: + +- `capacity` (Number): The total storage capacity available to the user, in bytes. +- `used` (Number): The amount of storage the user has used, in bytes. ## Examples +Show how much storage the user has used + ```html -``` \ No newline at end of file +``` diff --git a/src/docs/src/Hosting/list.md b/src/docs/src/Hosting/list.md index 5e339e7a1..d506de0e5 100755 --- a/src/docs/src/Hosting/list.md +++ b/src/docs/src/Hosting/list.md @@ -33,7 +33,7 @@ When the request includes `cursor` (even `null`) or `includeTotal`, the promise - `cursor` (String) (optional): Present while more pages exist; pass it to the next call. - `total` (Number) (optional): Present when `includeTotal` was set. -Requests without pagination params keep returning the full list as a plain array, so existing code is unaffected — under the hood the SDK now fetches it page by page. +Without pagination params, the full list is returned as a plain array. With `stream: true`, the method returns an async iterator of page objects instead: diff --git a/src/docs/src/KV.md b/src/docs/src/KV.md index 407504189..282e60f11 100644 --- a/src/docs/src/KV.md +++ b/src/docs/src/KV.md @@ -11,8 +11,6 @@ Puter.js handles all the infrastructure for you, so you don't need to set up ser
Need to share data across users? Each user's key-value store lives in their own account, so one user can't read another's data. To keep a single, centralized store that every user reads from and writes to, use a Serverless Worker — its code can act on the worker owner's resources, giving all users one shared backend.
-
Key layout is the access boundary. To let another account watch part of your store instead of copying it, mint an Events share handle over a key prefix. A handle pins the prefix it was granted on, so reorganizing your keys breaks every handle already given out — grant on a stable synthetic segment such as workspace:<uuid>: rather than a semantic one like q3-planning:, which is the kind of name that gets renamed.
- ## Features
@@ -225,6 +223,8 @@ These Key-Value Store features are supported out of the box when using Puter.js: - **[`puter.kv.expireAt()`](/KV/expireAt/)** - Set key expiration timestamp - **[`puter.kv.list()`](/KV/list/)** - List keys in ascending or descending order - **[`puter.kv.flush()`](/KV/flush/)** - Clear all data +- **[`puter.kv.MAX_KEY_SIZE`](/KV/MAX_KEY_SIZE/)** - Maximum key size in bytes +- **[`puter.kv.MAX_VALUE_SIZE`](/KV/MAX_VALUE_SIZE/)** - Maximum value size in bytes ## Examples diff --git a/src/docs/src/KV/list.md b/src/docs/src/KV/list.md index 5b9dcf397..5e1e45ba3 100755 --- a/src/docs/src/KV/list.md +++ b/src/docs/src/KV/list.md @@ -58,7 +58,7 @@ When paginating, iterate until the result has no `cursor` — a page may hold fe Invalid options, such as a non-boolean `reverse` or `offset` combined with `stream`, reject the returned promise with an `invalid_request` error; `list()` never throws synchronously. With `stream: true`, the iterator's first `next()` rejects instead. -Full (non-paginated) listings keep resolving to a plain array, so existing code is unaffected — under the hood the SDK now fetches them page by page. They still read the entire store, though: every page is metered, so on large stores a bare `list()` gets slow and costly (the SDK logs a one-time console warning when a full listing spans multiple pages). Prefer `stream: true` or explicit `limit`/`cursor` pages, and narrow the scan with a `pattern`. +Without pagination params, the full listing is returned as a plain array. It reads the entire store and is metered per page, so on large stores a bare `list()` gets slow and costly. The SDK logs a one-time console warning when a full listing spans multiple pages. Use `stream: true` or `limit` and `cursor` pages instead, and narrow the scan with a `pattern`. With `stream: true`, the method returns an async iterator of [`KVListPage`](/Objects/kvlistpage) objects instead: @@ -74,7 +74,7 @@ for await (const page of puter.kv.list({ pattern: 'log:*', stream: true })) { List keys in reverse order -```html;kv-list-reverse +```html diff --git a/src/docs/src/KV/set.md b/src/docs/src/KV/set.md index d8503ca8a..779fc7c66 100755 --- a/src/docs/src/KV/set.md +++ b/src/docs/src/KV/set.md @@ -38,7 +38,7 @@ A Unix timestamp in seconds at which the key should expire. Omit it, or pass `nu #### `disableSharing` (Boolean) (optional) -Pass inside the trailing options object — `set(key, value, { disableSharing: true })` — to mark this entry private to your app. A private entry cannot be read, listed, changed, or deleted by any other app, even one the user has granted access to your app's data with [`puter.perms.request('appData', …)`](/Perms/appData/). It is also not watched by other apps, or by another user through a [share handle](/Events/onLocal/#sharing-key-value-events-with-another-user): those subscriptions get no event, key, or value for it. Use it for anything another app should never see, such as a cached access token: a user approving a request cannot see what your store holds. +Pass inside the trailing options object — `set(key, value, { disableSharing: true })` — to mark this entry private to your app. A private entry cannot be read, listed, changed, or deleted by any other app, even one the user has granted access to your app's data with [`puter.perms.request('appData', …)`](/Perms/appData/). It is also not watched by other apps, or by another user through a [share handle](/Events/subjects/#sharing-key-value-events-with-another-user): those subscriptions get no event, key, or value for it. Use it for anything another app should never see, such as a cached access token: a user approving a request cannot see what your store holds. The batch form takes it too — `set([...items], { disableSharing: true })` marks every entry in the batch. diff --git a/src/docs/src/KV/update.md b/src/docs/src/KV/update.md index 50a8f5cf8..f77cecff3 100644 --- a/src/docs/src/KV/update.md +++ b/src/docs/src/KV/update.md @@ -59,7 +59,7 @@ A rejection carries an `Error` with a stable `code`: Update an element of a root array -```html;kv-update-root-array +```html diff --git a/src/docs/src/Objects.md b/src/docs/src/Objects.md index 72ffc9dfa..8213f9e36 100644 --- a/src/docs/src/Objects.md +++ b/src/docs/src/Objects.md @@ -11,6 +11,7 @@ Various object types and classes that represent different entities in the Puter - **[AppConnection](/Objects/AppConnection/)** - Represents a connection to an application - **[ChatResponse](/Objects/chatresponse/)** - Represents an AI chat response - **[ChatResponseChunk](/Objects/chatresponsechunk/)** - Represents a chunk of streaming chat response data +- **[CreateAppResult](/Objects/createappresult/)** - Represents the result of creating an app - **[DetailedAppUsage](/Objects/detailedappusage/)** - Represents detailed resource usage statistics for a specific application - **[EmailListPage](/Objects/emaillistpage/)** - Represents one page of mailbox listing results - **[EmailMessage](/Objects/emailmessage/)** - Represents a fully parsed message from the user's mailbox @@ -18,9 +19,12 @@ Various object types and classes that represent different entities in the Puter - **[EmailSummary](/Objects/emailsummary/)** - Represents one message in a mailbox listing - **[FSItem](/Objects/fsitem/)** - Represents a file or directory - **[KVPair](/Objects/kvpair/)** - Represents a key-value pair +- **[KVListPage](/Objects/kvlistpage/)** - Represents one page of key-value list results - **[MonthlyUsage](/Objects/monthlyusage/)** - Represents user's monthly resource usage information - **[PuterPeerConnection](/Objects/puterpeerconnection/)** - Represents a data-channel connection to a peer - **[PuterPeerServer](/Objects/puterpeerserver/)** - Represents a peer server and its connected clients +- **[SignInResult](/Objects/signinresult/)** - Represents the result of a sign-in +- **[SpaceInfo](/Objects/spaceinfo/)** - Represents the user's storage capacity and usage - **[Speech2TxtResult](/Objects/speech2txtresult/)** - Represents speech-to-text transcription results - **[Subdomain](/Objects/subdomain/)** - Represents a subdomain - **[Team](/Objects/team/)** - Represents a Puter team @@ -29,5 +33,6 @@ Various object types and classes that represent different entities in the Puter - **[TTSVoice](/Objects/ttsvoice/)** - Represents an available text-to-speech voice - **[ToolCall](/Objects/toolcall/)** - Represents a tool invocation request - **[User](/Objects/user/)** - Represents a Puter user +- **[UserProfile](/Objects/userprofile/)** - Represents a user's public profile - **[WorkerDeployment](/Objects/workerdeployment/)** - Represents a worker deployment result - **[WorkerInfo](/Objects/workerinfo/)** - Represents worker information diff --git a/src/docs/src/Peer.md b/src/docs/src/Peer.md index 125b3aa22..a70a968fd 100644 --- a/src/docs/src/Peer.md +++ b/src/docs/src/Peer.md @@ -131,6 +131,7 @@ These peer features are supported out of the box when using Puter.js: - **[`puter.peer.serve()`](/Peer/serve/)** - Create a peer server and generate an invite code - **[`puter.peer.connect()`](/Peer/connect/)** - Connect to a peer server using an invite code - **[`puter.peer.ensureTurnRelays()`](/Peer/ensureTurnRelays/)** - Preload TURN relays for faster connections +- **[`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/)** - Create a grant that lets a guest without a Puter account use Puter's relays ## Examples diff --git a/src/docs/src/Perms.md b/src/docs/src/Perms.md index 8edc2e64c..7f72a7ad4 100644 --- a/src/docs/src/Perms.md +++ b/src/docs/src/Perms.md @@ -6,9 +6,7 @@ platforms: [websites, apps] The Permissions API enables your application to request access to user data and resources such as email addresses, special folders (Desktop, Documents, Pictures, Videos), apps, subdomains, and other apps' saved data. -There are two methods. [`puter.perms.request()`](/Perms/request/) asks the user for access, and [`puter.perms.check()`](/Perms/check/) reports whether they have already granted it without prompting. - -Both take the same two arguments: the resource being asked about, and the details that resource needs. +Each request names a resource and the details that resource needs: ```js await puter.perms.request('email'); @@ -17,8 +15,6 @@ await puter.perms.request('apps', { access: 'read' }); await puter.perms.request('appData', { app: 'contacts', scopes: 'read' }); ``` -When requesting permissions, users will be prompted to grant or deny access. If a permission has already been granted, the user will not be prompted again. This provides a seamless experience while maintaining user privacy and control. - ## Features
@@ -190,22 +186,6 @@ Pass an array to ask for everything your app needs in one call. Anything already
-## Resources - -The resource decides which details the call takes and what a request resolves to: - -| Resource | Details | `request()` resolves to | -| -------- | ------- | ----------------------- | -| `'email'` | — | The user's email address | -| `'folder'` | `{ name, access }` | The folder's path | -| `'apps'` | `{ access }` | `true` if granted | -| `'subdomains'` | `{ access }` | `true` if granted | -| `'appData'` | `{ app, scopes }` | `true` if granted | -| `'appRootDir'` | `{ app, access }` | The app's root directory, or `undefined` if denied | -| `'permission'` | `{ permission }` or `{ permissions }` | `true` if granted | - -Anything denied resolves to a falsy value, so one `if` covers both outcomes. `access` is `'read'` (the default) or `'write'`. - ## Functions - **[`puter.perms.request()`](/Perms/request/)** - Request access to a resource, or to several at once diff --git a/src/docs/src/Perms/request.md b/src/docs/src/Perms/request.md index 9d8cb353a..d255d7206 100644 --- a/src/docs/src/Perms/request.md +++ b/src/docs/src/Perms/request.md @@ -6,9 +6,13 @@ platforms: [websites, apps] Request access to something belonging to the user. The first argument names the resource; the second carries the details that resource takes. -The user is prompted to allow or deny. Anything already granted is skipped, so a call whose access is fully in place doesn't prompt at all. [`puter.perms.check()`](/Perms/check/) reads the same state, so the two always agree. +
-Inside the Puter desktop the prompt is shown as a dialog. On websites, it opens in a popup window on the Puter origin — call this from a user gesture (e.g. a click handler) so the browser doesn't block the popup; without a gesture, a consent dialog is shown first and the popup opens when the user clicks Continue. +The user is prompted to grant or deny access. If they already granted it, they aren't prompted again, so a call whose access is fully in place doesn't prompt at all. [`puter.perms.check()`](/Perms/check/) reads the same state, so the two always agree. + +
+ +Inside the Puter desktop the prompt is shown as a dialog. On websites, it opens in a popup window on the Puter origin. Call this from a user gesture, such as a click handler, so the browser doesn't block the popup. Without a gesture, a consent dialog is shown first and the popup opens when the user clicks Continue. ## Syntax @@ -66,8 +70,6 @@ The array form resolves to an array of those values, in the order asked. Some permission strings are not supported and are denied silently. -An app's root directory has no raw form: `app-root-dir:` reads as nothing during a permission check, so asking for it this way would prompt on every call and [`check()`](/Perms/check/) would report it as not granted even once it is. Use the `'appRootDir'` resource, which asks the server directly. - A lone string that names no resource is treated as a permission string, so `puter.perms.request('fs:/user/Documents:read')` keeps working. ## Creating a path on request diff --git a/src/docs/src/UI/getLanguage.md b/src/docs/src/UI/getLanguage.md index 9d1a32cf1..c61140aea 100644 --- a/src/docs/src/UI/getLanguage.md +++ b/src/docs/src/UI/getLanguage.md @@ -4,7 +4,7 @@ description: Retrieves the current language/locale code from the Puter environme platforms: [apps] --- -Retrieves the current language/locale code from the Puter environment. This function communicates with the host environment to get the active language setting. +Retrieves the current language/locale code from the Puter environment. ## Syntax ```js diff --git a/src/docs/src/Workers/list.md b/src/docs/src/Workers/list.md index 7cda1a1d3..4a7893069 100644 --- a/src/docs/src/Workers/list.md +++ b/src/docs/src/Workers/list.md @@ -35,7 +35,7 @@ When the request includes any pagination option, the promise instead resolves to - `cursor` (String) (optional): Present while more pages exist; pass it to the next call. - `total` (Number) (optional): Present when `includeTotal` was set. -Requests without pagination params keep returning the full list as a plain array, so existing code is unaffected — under the hood the SDK now fetches it page by page. +Without pagination params, the full list is returned as a plain array. With `stream: true`, the method returns an async iterator of page objects instead: diff --git a/src/docs/src/rate-limits-and-quotas.md b/src/docs/src/rate-limits-and-quotas.md index a6708cd72..c9e9b97a3 100644 --- a/src/docs/src/rate-limits-and-quotas.md +++ b/src/docs/src/rate-limits-and-quotas.md @@ -361,7 +361,7 @@ The mutation and read budgets are per user, per app, not per team: administering ### Events -See [Events](/Events/) for an overview, [`onLocal()`](/Events/onLocal/#gaps) for gap markers, and [`onPersistent()`](/Events/onPersistent/#suspended-subscriptions) for suspension. +See [Events](/Events/) for an overview, [`onLocal()`](/Events/subjects/#gaps) for gap markers, and [`onPersistent()`](/Events/onPersistent/#suspended-subscriptions) for suspension. #### What an account can hold @@ -389,7 +389,7 @@ Counted per event, per region: - KV limits follow the plan of the **key's owner**, not the writer's or the subscribers'. If the owner's plan can't be looked up, the free numbers apply. A server with no metering uses the paid numbers. - They count subscriptions, not people. A user with three tabs open counts three times. -- Past the limit, the next subscriptions get a [gap marker](/Events/onLocal/#gaps) instead of the event (up to the same number again), and any after that get nothing. With 1,100 subscriptions on a paid owner's key: 512 get the event, 512 get a gap, 76 get nothing. +- Past the limit, the next subscriptions get a [gap marker](/Events/subjects/#gaps) instead of the event (up to the same number again), and any after that get nothing. With 1,100 subscriptions on a paid owner's key: 512 get the event, 512 get a gap, 76 get nothing. - The region that handles the write delivers to every persistent subscription plus its own connected clients. Other regions deliver only to their own connected clients. So persistent subscriptions share one budget per event, while session subscriptions spread across regions can reach more in total. - `includeValue` values are left out of every delivery for a change when more than **128** subscriptions match it (whatever the owner's plan), when the filter-check limit stops the count early, or when the value is over **16 KB**. diff --git a/src/docs/src/recipes/deploy-workers-programmatically.md b/src/docs/src/recipes/deploy-workers-programmatically.md index 56796e751..8d64695f1 100644 --- a/src/docs/src/recipes/deploy-workers-programmatically.md +++ b/src/docs/src/recipes/deploy-workers-programmatically.md @@ -33,8 +33,8 @@ const deployment = await puter.workers.create('notes-api', 'notes-api.js'); deployment.url; // 'https://notes-api.puter.work' ``` -Give it 5 to 30 seconds before the first request, while the worker spreads to -every edge server. Then call it with +Give it 5 to 30 seconds before the first request, while the worker deploys. +Then call it with [`puter.workers.exec()`](/Workers/exec/), which sends the signed-in user along so the route can use `user.puter`: @@ -46,11 +46,10 @@ await res.json(); // { username: 'grace' } [Build an API with a worker](/recipes/build-an-api/) covers what to put in the routes. -Worker names are global, like subdomains, and are stored in lowercase. A name -can use letters, numbers, hyphens and underscores. When another account already -has the name, `create()` rejects, so pick another one or use -[`puter.randName()`](/Utils/randName/). The account deploying the worker needs a -verified email address. +Worker names are global, like subdomains. When another account already has the +name, [`create()`](/Workers/create/) rejects, so pick another one or use +[`puter.randName()`](/Utils/randName/). All the naming rules and requirements +are on the [`puter.workers.create()`](/Workers/create/) page. ## Update Its Code diff --git a/src/docs/src/recipes/share-data-between-apps.md b/src/docs/src/recipes/share-data-between-apps.md index 68d555dfe..d74d1b406 100644 --- a/src/docs/src/recipes/share-data-between-apps.md +++ b/src/docs/src/recipes/share-data-between-apps.md @@ -32,13 +32,9 @@ const granted = await puter.perms.request('appData', { This returns `true` if the user allows it and `false` if they don't. Once allowed, later calls return `true` without asking again. -There are three levels of access. Only ask for what you need: - -| Scope | Lets your app call | -| --- | --- | -| `read` | `get`, `list` | -| `write` | `set`, `add`, `incr`, `decr`, `update` | -| `delete` | `del`, `remove`, `expire`, `expireAt` | +There are three levels of access, `read`, `write` and `delete`. Only ask for what +you need. The methods each one allows are listed in +[Using another app's data](/Perms/appData/). `write` doesn't include `delete`, so an app with `write` access can add and change entries but can't remove them. To ask for more than one, pass an array, @@ -144,4 +140,4 @@ const setPrivate = (key, value) => puter.kv.set(key, value, { disableSharing: tr through [Events](/Events/). - This is for sharing between apps for the same user. To share data between different users, use a [worker](/recipes/store-server-side-data/) or an - [Events share handle](/Events/onLocal/#sharing-key-value-events-with-another-user). + [Events share handle](/Events/subjects/#sharing-key-value-events-with-another-user). diff --git a/src/docs/src/recipes/work-with-a-large-directory.md b/src/docs/src/recipes/work-with-a-large-directory.md index 224fbfa8a..9fc462bbe 100644 --- a/src/docs/src/recipes/work-with-a-large-directory.md +++ b/src/docs/src/recipes/work-with-a-large-directory.md @@ -66,9 +66,9 @@ const items = await puter.fs.readdir({ }); ``` -The `sortBy` option takes `name` (the default), `modified`, `type` or `size`, -and `sortOrder` takes `asc` (the default) or `desc`. When you read pages with a -cursor, every page must use the same sort as the first one. +All the sort options are listed on [`puter.fs.readdir()`](/FS/readdir/). When +you read pages with a cursor, every page must use the same sort as the first +one. ## Include Subdirectories diff --git a/src/docs/src/sidebar.js b/src/docs/src/sidebar.js index d115450db..abdb5bce5 100755 --- a/src/docs/src/sidebar.js +++ b/src/docs/src/sidebar.js @@ -358,6 +358,14 @@ let sidebar = [ source: '/FS/stat.md', path: '/FS/stat', }, + { + title: 'space()', + page_title: 'puter.fs.space()', + title_tag: 'puter.fs.space()', + icon: '/assets/img/function.svg', + source: '/FS/space.md', + path: '/FS/space', + }, { title: 'delete()', page_title: 'puter.fs.delete()', @@ -414,6 +422,14 @@ let sidebar = [ source: '/FS/listShared.md', path: '/FS/listShared', }, + { + title: 'listSharedByMe()', + page_title: 'puter.fs.listSharedByMe()', + title_tag: 'puter.fs.listSharedByMe()', + icon: '/assets/img/function.svg', + source: '/FS/listSharedByMe.md', + path: '/FS/listSharedByMe', + }, { title: 'getShares()', page_title: 'puter.fs.getShares()', @@ -502,6 +518,14 @@ let sidebar = [ source: '/Events/workers.md', path: '/Events/workers', }, + { + title: 'Subjects and events', + page_title: 'Subjects and events', + title_tag: 'Events Subjects', + icon: '/assets/img/object.svg', + source: '/Events/subjects.md', + path: '/Events/subjects', + }, ], }, { @@ -633,14 +657,6 @@ let sidebar = [ source: '/Email.md', path: '/Email', children: [ - { - title: 'sendTransactional()', - page_title: 'puter.email.sendTransactional()', - title_tag: 'puter.email.sendTransactional()', - icon: '/assets/img/function.svg', - source: '/Email/sendTransactional.md', - path: '/Email/sendTransactional', - }, { title: 'list()', page_title: 'puter.email.list()', @@ -657,6 +673,14 @@ let sidebar = [ source: '/Email/get.md', path: '/Email/get', }, + { + title: 'sendTransactional()', + page_title: 'puter.email.sendTransactional()', + title_tag: 'puter.email.sendTransactional()', + icon: '/assets/img/function.svg', + source: '/Email/sendTransactional.md', + path: '/Email/sendTransactional', + }, ], }, { @@ -1374,6 +1398,13 @@ let sidebar = [ source: '/Objects/signinresult.md', path: '/Objects/signinresult', }, + { + title: 'SpaceInfo', + title_tag: 'SpaceInfo', + icon: '/assets/img/object.svg', + source: '/Objects/spaceinfo.md', + path: '/Objects/spaceinfo', + }, { title: 'Speech2TxtResult', title_tag: 'Speech2TxtResult',