Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .claude/skills/sdk-docs-writing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ Docs in this repo are **auto-generated** from JSDoc comments in TypeScript sourc

You write JSDoc. The tooling produces the final pages.

Two things follow from that, and they govern everything below.

**This JSDoc is published prose.** It renders into the public SDK reference, so it follows the same style rules as the rest of the Base44 docs. Read `mintlify-docs/.claude/skills/base44-docs-writing/SKILL.md` first, and treat the writing guidance here as the SDK-specific additions to it.

**Fix the source, never the generated MDX.** Every run overwrites `developers/references/sdk/docs/` in `mintlify-docs`, so an edit made there is gone on the next regeneration. When something reads wrong on a published page, the fix belongs in the JSDoc that produced it.

## Where docs come from

| File pattern | Role |
|---|---|
| `src/modules/*.types.ts` | **Public API surface** — JSDoc here becomes the published docs |
| `src/modules/*.ts` | Implementation — mark with `@internal` to hide from docs |
| `src/modules/*.types.ts` | **Public API surface.** JSDoc here becomes the published docs |
| `src/modules/*.ts` | Implementation. Mark with `@internal` to hide from docs |
| `src/client.types.ts` | Client factory types |
| `src/types.ts` | Shared types |

Expand Down Expand Up @@ -75,6 +81,8 @@ Every public method needs: description, `@param` tags, `@returns`, and at least
## Writing style

- **Developer audience.** These are SDK reference docs for JavaScript/TypeScript developers.
- **Never use em dashes.** Use a comma or a separate sentence instead. Colons are allowed but should be rare, so reach for one only to introduce a genuine list.
- **Grammar and punctuation must be correct.** This is public-facing professional documentation. Two slips come up often when rewriting an em dash away. Do not put a comma before a restrictive `because` clause, and do not leave a sentence fragment behind when you split one sentence into two.
- **Concise descriptions.** First sentence is a verb phrase: "Lists records...", "Creates a new...", "Sends an invitation...".
- **Sentence case** for free-text headings in JSDoc.
- **State environment constraints** when a method is browser-only: "Requires a browser environment and can't be used in the backend."
Expand All @@ -92,5 +100,6 @@ Every public method needs: description, `@param` tags, `@returns`, and at least
1. **JSDoc completeness:** Every public method has description, `@param`, `@returns`, and `@example`.
2. **`@internal` on implementation:** Factory functions, config interfaces, and helpers are marked `@internal`.
3. **Examples work:** Code examples are syntactically valid TypeScript and use the `base44.` call path.
4. **Pipeline config:** New public types are in `types-to-expose.json`. Helper types that belong on another page are in `appended-articles.json`.
5. **Generate and review:** Run `npm run create-docs` and check the output renders correctly.
4. **No em dashes:** `grep -rn '—' src/` comes back empty for any file you touched.
5. **Pipeline config:** New public types are in `types-to-expose.json`. Helper types that belong on another page are in `appended-articles.json`.
6. **Generate and review:** Run `npm run create-docs` and check the output renders correctly.
9 changes: 8 additions & 1 deletion scripts/mintlify-post-processing/appended-articles.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
"interfaces/ConnectorsModule": [
"type-aliases/ConnectorIntegrationType",
"interfaces/ConnectorIntegrationTypeRegistry",
"interfaces/UserConnectorsModule"
"interfaces/UserConnectorsModule",
"interfaces/ConnectorApiRequest",
"type-aliases/ConnectorApiQueryValue",
"interfaces/ConnectorApiResponse",
"type-aliases/ConnectorApiResponsePhase"
],
"interfaces/AppModule": [
"interfaces/AppPublicSettingsResponse"
],
"type-aliases/EntitiesModule": [
"interfaces/EntityHandler",
Expand Down
15 changes: 14 additions & 1 deletion scripts/mintlify-post-processing/copy-to-local-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ Examples:
// Target location within mintlify-docs for SDK reference docs
const SDK_DOCS_TARGET_PATH = "developers/references/sdk/docs";

/** Compare two nav page paths by the page name a reader actually sees. */
function byPageName(a, b) {
return a
.split("/")
.pop()
.localeCompare(b.split("/").pop(), "en", { sensitivity: "base" });
}

function scanSdkDocs(sdkDocsDir) {
const result = {};

Expand Down Expand Up @@ -139,7 +147,12 @@ function updateDocsJson(repoDir, sdkFiles) {
return Array.from(groupMap.entries()).map(([group, pages]) => ({
group,
expanded: true,
pages: pages.sort(),
// Sort on the page name, not the full path. A module lands in
// interfaces/ or type-aliases/ depending on how it is declared, which is
// invisible to the reader, and sorting on the path groups by that
// instead: every interfaces/ module first, then the alphabet restarting
// for the type-aliases/ ones.
pages: pages.sort((a, b) => byPageName(a, b)),
}));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,15 @@ function generateDocsJson(docsContent) {

if (existingGroup) {
existingGroup.pages.push(...docsContent.typeAliases);
existingGroup.pages.sort(); // Sort combined pages alphabetically
// Sort on the page name rather than the full path, so a module declared
// as a type alias sorts next to its neighbours instead of after every
// interface-declared one.
existingGroup.pages.sort((a, b) =>
a
.split("/")
.pop()
.localeCompare(b.split("/").pop(), "en", { sensitivity: "base" })
);
} else {
groups.push({
group: groupName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[
"actors",
"AiGatewayConnection",
"DeleteManyResult",
"DeleteResult",
Expand Down
3 changes: 2 additions & 1 deletion scripts/mintlify-post-processing/types-to-expose.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
"AppModule",
"AppPublicSettingsResponse",
"AuthModule",
"ConnectorApiQueryValue",
"ConnectorApiRequest",
"ConnectorApiResponse",
"ConnectorApiResponsePhase",
"ConnectorIntegrationType",
"ConnectorIntegrationTypeRegistry",
"ConnectorsModule",
"CoreIntegrations",
"CustomIntegrationsModule",
"DeleteManyResult",
"DeleteResult",
Expand All @@ -27,7 +29,6 @@
"FunctionsModule",
"ImportResult",
"IntegrationsModule",
"CoreIntegrations",
"SortField",
"SsoModule",
"UpdateManyResult"
Expand Down
14 changes: 7 additions & 7 deletions src/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
* export class MyActor extends Actor { ... }
*
* At deploy time the bundler replaces this import with the compiled
* Cloudflare Durable Object implementation — this file provides types only.
* Cloudflare Durable Object implementation. This file provides types only.
*/

import type { Base44Client } from "./client";

/**
* A single client connection. `Send` is the message type this connection accepts
* via {@link send} the actor's *outgoing* (server→client) messages.
* via {@link send}, the actor's *outgoing* (server→client) messages.
*/
export interface Conn<Send = unknown> {
/** Unique per-connection id (one per socket/tab), the same value the client
Expand All @@ -37,9 +37,9 @@ export interface Storage {
* Base class for an Actor.
*
* @typeParam Incoming - messages this actor *receives* from clients
* (`handleMessage`'s `msg`) the schema's `toServer` section.
* (`handleMessage`'s `msg`), the schema's `toServer` section.
* @typeParam Outgoing - messages this actor *sends* to clients
* (`conn.send`/`broadcast`) the schema's `toClient` section.
* (`conn.send`/`broadcast`), the schema's `toClient` section.
*
* With a generated `schema.jsonc`, wire both from the registry so they can't drift
* from the client's types:
Expand All @@ -56,7 +56,7 @@ export abstract class Actor<Incoming = unknown, Outgoing = unknown> {

/**
* Optional wake hook: runs once when the instance starts, before any
* connection is handled safe to load persisted state here.
* connection is handled. It is safe to load persisted state here.
*/
handleStart(): void | Promise<void> {}

Expand Down Expand Up @@ -84,7 +84,7 @@ export abstract class Actor<Incoming = unknown, Outgoing = unknown> {
/**
* Managed ticker (opt-in). Override {@link shouldTick} and the platform runs
* {@link handleTick} on a timer of {@link tickIntervalMs} while it returns true,
* and stops (letting the Durable Object hibernate no compute cost) when it
* and stops (letting the Durable Object hibernate at no compute cost) when it
* returns false. The platform owns scheduling, rescheduling, self-heal, and
* error-safety.
*
Expand All @@ -111,7 +111,7 @@ export abstract class Actor<Incoming = unknown, Outgoing = unknown> {
}

/**
* Anonymous Base44 client scoped to this actor instance no user or service
* Anonymous Base44 client scoped to this actor instance, with no user or service
* auth, so entity access is RLS-gated (same as a logged-out visitor). Always
* operates on production data: an actor runs server-side with no per-connection
* identity, so a Test DB preview selected in the editor does not apply here.
Expand Down
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export function createClient(config: CreateClientConfig): Base44Client {

// Dedicated client for actor connection-token mints: no onError (a legacy
// actor answers every mint with an expected 409 before the proxy fallback,
// which must not reach the app's error handler the actors module forwards
// which must not reach the app's error handler; the actors module forwards
// genuine failures itself via onMintError) and no constructor token
// (auth is per-request so a login/logout is picked up on every reconnect).
const actorsAxiosClient = createAxiosClient({
Expand Down
4 changes: 2 additions & 2 deletions src/client.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ export interface CreateClientOptions {
* Optional error handler that will be called whenever an API error occurs.
*
* Also receives {@link ActorsModule | actors} connection failures. Errors
* are usually {@linkcode Base44Error} instances check `error.status`.
* are usually {@linkcode Base44Error} instances, so check `error.status`.
*/
onError?: (error: Error) => void;
/**
* Forces the actors transport. `"auto"` (default) connects directly to the
* actor and falls back to the platform proxy when the app's actors don't
* support direct connections; `"proxy"` always uses the platform proxy
* (ops rollback no connection-token calls); `"direct"` disables the
* (ops rollback, with no connection-token calls); `"direct"` disables the
* fallback (validation environments).
* @internal
*/
Expand Down
12 changes: 6 additions & 6 deletions src/modules/actors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ const DEAD_MS = 3_000;
// 422 = no principal (e.g. anonymous outside a browser) or an id/room only the
// proxy's looser validation accepts, 405 = a backend that predates the mint
// endpoint (its actor deploy routes catch the path via `{handler_name:path}`
// but not the POST method and the real endpoint never 405s a POST). The
// but not the POST method, and the real endpoint never 405s a POST). The
// proxy serves migrated actors too, so falling back is always safe.
const PROXY_FALLBACK_STATUSES = new Set([405, 409, 422, 503]);

// Mint responses no retry can fix (bad request / forbidden / not found): the
// connection closes instead of re-minting forever; a fresh connect() re-probes.
// 401 is deliberately absent — the auth token is re-read on every attempt, so a
// 401 is deliberately absent. The auth token is re-read on every attempt, so a
// login recovers on the next retry. Disjoint from PROXY_FALLBACK_STATUSES.
const TERMINAL_MINT_STATUSES = new Set([400, 403, 404]);

Expand All @@ -80,15 +80,15 @@ function toError(err: unknown): Error {

/**
* A live connection to an actor instance. Only obtainable from
* {@link ActorRef.connect}, so `subscribe`/`send` are always valid — the socket
* {@link ActorRef.connect}, so `subscribe`/`send` are always valid. The socket
* exists for this object's whole lifetime.
*/
class Connection {
private readonly ws: ReconnectingWebSocket;
private readonly listeners = new Set<(data: unknown) => void>();
private heartbeat: ReturnType<typeof setInterval> | null = null;
private closed = false;
/** The client-chosen conn id becomes _pk → the actor's conn.id. */
/** The client-chosen conn id. It becomes _pk → the actor's conn.id. */
readonly id: string;

constructor(
Expand All @@ -104,7 +104,7 @@ class Connection {
// mint answers with a fallback status the choice is sticky for this
// socket's lifetime (a fresh connect() after close() probes direct again,
// picking up actors migrated in the meantime). Any other mint failure
// rejects, which ReconnectingWebSocket retries with backoff except the
// rejects, which ReconnectingWebSocket retries with backoff, except the
// terminal statuses, which close this connection for good.
let useProxy = config.transport === "proxy";
const urlProvider = async (): Promise<string> => {
Expand Down Expand Up @@ -245,7 +245,7 @@ function makeActorRef(
* The legacy platform-proxy URL, byte-for-byte what PartySocket built before
* the direct path existed: same scheme swap (including its localhost-needs-a-
* port quirk), case-preserved party segment, `_pk` first in the query. The
* `handler` param is load-bearing — the proxy reads it for the actor name.
* `handler` param is load-bearing. The proxy reads it for the actor name.
*/
export function buildProxyActorUrl(
rawHost: string,
Expand Down
14 changes: 7 additions & 7 deletions src/modules/actors.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface ActorRegistry {}

/**
* Auto-populated by `base44 types generate` with the names of your deployed actors.
* Do not edit this interface manually — use {@link ActorRegistry} for message types.
* Do not edit this interface manually. Use {@link ActorRegistry} for message types.
*/
export interface ActorNameRegistry {}

Expand All @@ -42,7 +42,7 @@ type ToServerFor<N extends string> = N extends keyof ActorRegistry
/** Options for {@link ActorRef.connect}. */
export interface ActorConnectOptions {
/**
* The connection id — becomes the actor's `conn.id`. Supply a stable value
* The connection id, used as the actor's `conn.id`. Supply a stable value
* (e.g. persisted per tab) so a reconnect reuses the same server-side
* identity; omit for an auto-generated per-connection id.
*/
Expand All @@ -57,7 +57,7 @@ export interface ActorSubscription {

/**
* A live connection to an actor instance, returned by {@link ActorRef.connect}.
* `subscribe`/`send` are always valid — you only get a `Connection` once the
* `subscribe`/`send` are always valid. You only get a `Connection` once the
* socket has been opened, so there's no pre-connect state to guard against.
*/
export interface Connection<N extends string = string> {
Expand All @@ -73,14 +73,14 @@ export interface Connection<N extends string = string> {

/**
* Tear down the socket, heartbeat, and all listeners. Safe to call more
* than once. A connection also closes itself when it fails permanently
* see {@link ActorRef.connect}.
* than once. A connection also closes itself when it fails permanently.
* See {@link ActorRef.connect}.
*/
close(): void;
}

/**
* A handle to one actor instance `base44.actors.MyActor(id)`. Call
* A handle to one actor instance, obtained from `base44.actors.MyActor(id)`. Call
* {@link connect} to open the socket and get a {@link Connection}.
*/
export interface ActorRef<N extends string = string> {
Expand All @@ -97,7 +97,7 @@ export interface ActorRef<N extends string = string> {
}

/**
* Client for a single named Actor — call it with an instance id to get an
* Client for a single named Actor. Call it with an instance id to get an
* {@link ActorRef}. Typed automatically when the actor is registered in
* {@link ActorRegistry}.
*/
Expand Down
4 changes: 2 additions & 2 deletions src/modules/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const analyticsSharedState = getSharedInstance(
wasInitializationTracked: false,
sessionContext: null as SessionContext | null,
sessionStartTime: null as string | null,
// Memoized session id for when `localStorage` can't persist one — see
// Memoized session id for when `localStorage` can't persist one. See
// getAnalyticsSessionId.
fallbackSessionId: null as string | null,
config: {
Expand Down Expand Up @@ -342,7 +342,7 @@ async function getSessionContext(
// With no token there is no identity to resolve: `me()` can only answer 401,
// which the browser logs to the console before any handler here sees it. On
// a public page that request is the sole reason an error appears, so skip
// it. This is not memoized — a visitor who logs in later must still resolve.
// it. This is not memoized. A visitor who logs in later must still resolve.
if (!userAuthModule.hasToken()) {
return { user_id: null, session_id: getAnalyticsSessionId() };
}
Expand Down
10 changes: 5 additions & 5 deletions src/modules/app.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type AppPublicSettings =
| string;

/**
* The app's public configuration, as returned by {@link AppModule.getPublicSettings}.
* The app's public configuration, as returned by `getPublicSettings()`.
*/
export interface AppPublicSettingsResponse {
/** The app's ID. */
Expand All @@ -28,17 +28,17 @@ export interface AppPublicSettingsResponse {
* ## Authentication Modes
*
* This module is available to use with a client in all authentication modes. The
* client's token, when it has one, is sent with the request a signed-in visitor
* client's token, when it has one, is sent with the request, so a signed-in visitor
* who has no access to the app is reported differently from an anonymous one.
*/
export interface AppModule {
/**
* Get the app's public configuration.
*
* Rejects with a {@linkcode Base44Error} when the visitor may not open the app:
* `status` is `403` and `data.extra_data.reason` says why — `"auth_required"`
* when the visitor must sign in, `"user_not_registered"` when the signed-in
* visitor has no access to this app.
* `status` is `403` and `data.extra_data.reason` says why. It is
* `"auth_required"` when the visitor must sign in, and
* `"user_not_registered"` when the signed-in visitor has no access to this app.
*
* @returns Promise resolving to the app's ID and access policy.
*
Expand Down
2 changes: 1 addition & 1 deletion src/modules/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export function createAuthModule(
// two identical GETs, so the second pays the first's full latency on every
// cold load.
//
// This shares the pending promise only it is cleared as soon as the request
// This shares the pending promise only, and it is cleared as soon as the request
// settles, so no resolved user is ever retained. Caching the user across
// requests would leave the app rendering a stale identity after logout or a
// session swap.
Expand Down
Loading
Loading