naro-js 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ import type { DatabaseShape, RowOf, TableName, TablesOf, UntypedDatabase } from "../types.js";
2
+ import type { PostgresChangesFilter, RealtimeBroadcastMessage, RealtimeChannelOptions, RealtimeChannelState, RealtimePostgresChangesPayload, RealtimePresenceJoinPayload, RealtimePresenceLeavePayload, RealtimePresenceState, RealtimeResult, RealtimeSendResult, RealtimeStatusCallback, RealtimeSystemMessage } from "./types.js";
3
+ /** The row type a change of `Table` carries: the generated Row, or any for `*` and untyped clients. */
4
+ type ChangeRow<Database, Table> = Table extends TableName<Database> ? RowOf<TablesOf<Database>[Table]> : Record<string, unknown>;
5
+ export declare function warnInDevelopment(message: string): void;
6
+ /**
7
+ * One subscription as the join carries it (original spec §7.3): keys this
8
+ * server does not take go too, so the hub names them (결정 15); an empty
9
+ * filter is none.
10
+ */
11
+ export declare function changeSpec(filter: PostgresChangesFilter): Record<string, unknown>;
12
+ /** The wire's `data` (Supabase's `type`, `record`, `old_record`) as supabase-js hands it over. */
13
+ export declare function changePayload(data: Record<string, unknown>): RealtimePostgresChangesPayload;
14
+ export declare class RealtimeChannel<Database extends DatabaseShape<Database> = UntypedDatabase> {
15
+ #private;
16
+ readonly name: string;
17
+ readonly topic: string;
18
+ readonly isPrivate: boolean;
19
+ readonly options: RealtimeChannelOptions;
20
+ get state(): RealtimeChannelState;
21
+ /**
22
+ * A listener (original spec §15), as supabase-js names them. Row changes
23
+ * and presence count from the next join; broadcast and system at once.
24
+ */
25
+ on<Table extends TableName<Database> | "*" = TableName<Database> | "*">(type: "postgres_changes", filter: PostgresChangesFilter<Table>, callback: (payload: RealtimePostgresChangesPayload<ChangeRow<Database, Table>>) => void): this;
26
+ on(type: "broadcast", filter: {
27
+ event: string;
28
+ }, callback: (message: RealtimeBroadcastMessage) => void): this;
29
+ on(type: "presence", filter: {
30
+ event: "sync";
31
+ }, callback: () => void): this;
32
+ on(type: "presence", filter: {
33
+ event: "join";
34
+ }, callback: (payload: RealtimePresenceJoinPayload) => void): this;
35
+ on(type: "presence", filter: {
36
+ event: "leave";
37
+ }, callback: (payload: RealtimePresenceLeavePayload) => void): this;
38
+ on(type: "system", filter: Record<string, never>, callback: (message: RealtimeSystemMessage) => void): this;
39
+ subscribe(callback?: RealtimeStatusCallback): this;
40
+ unsubscribe(): Promise<RealtimeResult>;
41
+ /**
42
+ * Broadcasts `message` on the join, or over HTTP before it: the server's
43
+ * answer, `lost` when the connection went before it (RealtimeSendResult).
44
+ */
45
+ send(message: RealtimeBroadcastMessage): Promise<RealtimeSendResult>;
46
+ /**
47
+ * Tracks `state` as this connection's presence here (original spec
48
+ * §11.1): the hub's answer. Only on a join — before it, NOT_JOINED, and
49
+ * nothing is kept for later (결정 18). It goes as its JSON reads back, and
50
+ * that copy, not the app's object, is what a later join sends again.
51
+ * While who the connection is is not settled (a new token in flight, a
52
+ * retry), it waits until it is (정정 (k)): the hub may yet refuse the new
53
+ * token for now and keep the last user, whose presence it would be.
54
+ */
55
+ track(state: Record<string, unknown>): Promise<RealtimeSendResult>;
56
+ /**
57
+ * Takes this connection's presence off the channel: the hub's answer, or
58
+ * none when not joined. After the app's tracks that wait, if any.
59
+ */
60
+ untrack(): Promise<RealtimeSendResult>;
61
+ /** The presences of the channel, by key (결정 16): a copy. */
62
+ presenceState(): RealtimePresenceState;
63
+ }
64
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { AuthClient } from "../auth/client.js";
2
+ import type { HttpConfig } from "../http.js";
3
+ import type { DatabaseShape, UntypedDatabase } from "../types.js";
4
+ import { RealtimeChannel } from "./channel.js";
5
+ import type { RealtimeChannelOptions, RealtimeOptions, RealtimeResult } from "./types.js";
6
+ export declare class RealtimeClient<Database extends DatabaseShape<Database> = UntypedDatabase> {
7
+ #private;
8
+ constructor(input: {
9
+ http: HttpConfig;
10
+ auth: AuthClient;
11
+ service: boolean;
12
+ options: RealtimeOptions;
13
+ });
14
+ channel(name: string, options?: RealtimeChannelOptions): RealtimeChannel<Database>;
15
+ getChannels(): RealtimeChannel<Database>[];
16
+ removeChannel(channel: RealtimeChannel<Database>): Promise<RealtimeResult>;
17
+ removeAllChannels(): Promise<RealtimeResult>;
18
+ }
@@ -0,0 +1,44 @@
1
+ /** 1, 2, 5, then 10 seconds, each times 0.5–1.5 (original spec §15). */
2
+ export declare const BACKOFF_STEPS_MS: readonly [1000, 2000, 5000, 10000];
3
+ /** 4029's floor: a refused client must not knock faster (design §8.5). */
4
+ export declare const RATE_LIMITED_MIN_MS = 5000;
5
+ /** The dispatcher's per-address window is a fixed minute (3-2c-2 결정 9). */
6
+ export declare const IP_LIMIT_WAIT_MS = 60000;
7
+ /**
8
+ * A restart closes everyone at once, so every 4503 return waits 0–10 s (design
9
+ * D6). The wait does not grow with the attempt count or the unopened tries:
10
+ * during a lasting BACKEND_UNAVAILABLE a client comes back about every 5 s on
11
+ * average, without slowing down.
12
+ */
13
+ export declare const RESTART_JITTER_MS = 10000;
14
+ /** Tries in a row that never opened before the client slows down (known-issues #271). */
15
+ export declare const UNOPENED_SLOWDOWN_AFTER = 6;
16
+ export declare const UNOPENED_DELAY_MS = 60000;
17
+ export type CloseAction = {
18
+ kind: "stop";
19
+ } | {
20
+ kind: "retry";
21
+ minMs: number;
22
+ } | {
23
+ kind: "session";
24
+ } | {
25
+ kind: "restart";
26
+ } | {
27
+ kind: "wait";
28
+ ms: number;
29
+ };
30
+ /**
31
+ * `reason` is the close's reason, or the code of the `system` frame sent
32
+ * just before it — the two are the same string (data-core `closeFrame`, the
33
+ * dispatcher's `realtime-refuse.ts`). A code this table does not know is
34
+ * tried again like 4500: a server newer than this SDK can always refuse again.
35
+ */
36
+ export declare function closeAction(code: number, reason: string): CloseAction;
37
+ export declare function backoffMs(attempt: number, random: () => number): number;
38
+ /** How long to wait before the next try after `action`. */
39
+ export declare function delayAfter(action: Exclude<CloseAction, {
40
+ kind: "stop";
41
+ }>, state: {
42
+ attempt: number;
43
+ unopened: number;
44
+ }, random: () => number): number;
@@ -0,0 +1,68 @@
1
+ import type { TokenStore } from "../auth/storage.js";
2
+ import { type RealtimeSocket } from "./socket.js";
3
+ export type IdentityConfig = {
4
+ tokens: TokenStore;
5
+ accessJwt: TokenStore | undefined;
6
+ service: boolean;
7
+ socket: () => RealtimeSocket;
8
+ /** Private channels the connection holds: each is judged again on a change. */
9
+ privateChannels: () => number;
10
+ /**
11
+ * The connection is live (`isLive()`) again: the joins that waited may go.
12
+ * Called whenever it becomes live — on every connection, and after every
13
+ * store read that ended in a settled identity — whether or not anything
14
+ * changed (a write of the same value, a retry with nothing to tell) and
15
+ * whether or not the hub took it (a refusal not tried again settles it
16
+ * too). The receiver must be idempotent.
17
+ */
18
+ onLive: () => void;
19
+ /**
20
+ * `auth.ready()`: resolves once a sign-in the page URL carried has been
21
+ * traded for its session (or failed). Each read of the store waits for it.
22
+ */
23
+ ready: () => Promise<unknown>;
24
+ };
25
+ /** A connection may change its token as fast as its private channels are judged again: 100 at once, then 2 a second (3-2c-1). */
26
+ export declare function rejudgeWaitMs(privateChannels: number): number;
27
+ export declare class Identity {
28
+ #private;
29
+ constructor(config: IdentityConfig);
30
+ /** The token the hub took on this connection; null for anon or before its answer. */
31
+ confirmed(): string | null;
32
+ /**
33
+ * The token the hub will hold once what was sent is handled — the last
34
+ * frame in flight, or else the one it took: the hub handles the
35
+ * connection's frames in order, so a frame sent now is judged for it.
36
+ * A frame in flight may yet be refused; the hub then keeps `confirmed()`.
37
+ */
38
+ heldToken(): string | null;
39
+ /**
40
+ * Nothing about who the connection is is pending — no read, no frame in
41
+ * flight, no retry, no refusal: a join sent now is judged for the identity
42
+ * the hub settled on, the stored one unless it refused that for good on
43
+ * this connection (결정 24).
44
+ */
45
+ isLive(): boolean;
46
+ /**
47
+ * A new connection: anon until told. Resolves once the stored token is
48
+ * sent, or found to need no frame; `onLive` says when the hub took it.
49
+ */
50
+ opened(): Promise<void>;
51
+ /** The stored token may have changed: tell the open connection what is new. */
52
+ changed(): Promise<void>;
53
+ /**
54
+ * A 4401: forget the stored session only if the hub answered
55
+ * INVALID_SESSION to it by name, and it is still stored — a sign-in since
56
+ * then keeps its new token (결정 3). A 4401 with no such answer clears
57
+ * nothing (plan 2026-10-28 정정): the hub keeps the expiry it read when it
58
+ * took the token (known-issues #183) while the backend's sign-in slides
59
+ * the session's under the same token, so a tab open for days is closed
60
+ * with a session that is still good. The next connection tells the token
61
+ * again, and hears INVALID_SESSION — and clears it — if it really is
62
+ * dead. The access JWT goes with the session it was minted for, so only
63
+ * when that session went.
64
+ */
65
+ refused(): Promise<void>;
66
+ /** The connection is gone: what it was told is forgotten. */
67
+ closed(): void;
68
+ }
@@ -0,0 +1,27 @@
1
+ import type { RealtimePresenceEntry, RealtimePresenceState } from "./types.js";
2
+ /**
3
+ * What join and leave say, as supabase-js's RealtimePresence does: a join's
4
+ * `currentPresences` are what the key held before it (none for a key's
5
+ * first), a leave's what it holds after it (plan 정정 (j)).
6
+ */
7
+ export type PresenceEmit = {
8
+ join(key: string, newPresences: RealtimePresenceEntry[], currentPresences: RealtimePresenceEntry[]): void;
9
+ leave(key: string, leftPresences: RealtimePresenceEntry[], currentPresences: RealtimePresenceEntry[]): void;
10
+ sync(): void;
11
+ };
12
+ export declare class Presence {
13
+ #private;
14
+ state(): RealtimePresenceState;
15
+ /** A new join, or the connection dropped: what is here stays until its presence_state. */
16
+ unsync(): void;
17
+ clear(): void;
18
+ /**
19
+ * No presence_state will say what is here any more (a join that may not
20
+ * read presence, one refused, the server's close, a stop — plan 정정
21
+ * (k)): each key leaves, then sync — said only when someone was here. A
22
+ * diff waits for the next presence_state.
23
+ */
24
+ leaveAll(emit: PresenceEmit): void;
25
+ applyState(payload: unknown, emit: PresenceEmit): void;
26
+ applyDiff(payload: unknown, emit: PresenceEmit): void;
27
+ }
@@ -0,0 +1,78 @@
1
+ export declare const REALTIME_PROTOCOL_VERSION = "1.0.0";
2
+ export declare const PHOENIX_TOPIC = "phoenix";
3
+ export declare const REALTIME_TOPIC_PREFIX = "realtime:";
4
+ /**
5
+ * The heartbeat byte for byte: the runtime answers exactly this for the hub
6
+ * (`setWebSocketAutoResponse`), so it never wakes a hibernating hub. Any other
7
+ * spelling is an ordinary frame that wakes it every 25 seconds.
8
+ */
9
+ export declare const HEARTBEAT_REQUEST = "{\"topic\":\"phoenix\",\"event\":\"heartbeat\",\"payload\":{},\"ref\":\"hb\"}";
10
+ export declare const HEARTBEAT_REF = "hb";
11
+ export declare const MAX_CHANNEL_NAME_LENGTH = 128;
12
+ /**
13
+ * One connection's frame bucket on the hub (data-core realtime-limits.ts):
14
+ * 10 a second, 120 deep, full at the open. A frame over it closes the
15
+ * connection with 4008 (CLIENT_FLOODING); the heartbeat does not count —
16
+ * the runtime answers it. socket.ts paces its frames to it.
17
+ */
18
+ export declare const CONNECTION_FRAMES_PER_SECOND = 10;
19
+ export declare const CONNECTION_FRAME_BURST = 120;
20
+ /**
21
+ * The refusals the hub warns a connection for beside the refusal — the
22
+ * project's joins, message and presence budgets (its `warn`: `tooManyJoins`,
23
+ * also for its queue of ordered frames full, and the broadcast and presence
24
+ * handlers) — and `FLOOD_WARNINGS` of them within `FLOOD_WINDOW_MS` close the
25
+ * connection with 4008 (data-core realtime-limits.ts). wire.test.ts reads
26
+ * the hub's source for every warning it gives.
27
+ */
28
+ export declare const WARNED_CODES: ReadonlySet<string>;
29
+ export declare const FLOOD_WARNINGS = 10;
30
+ export declare const FLOOD_WINDOW_MS = 60000;
31
+ /** The most channels of one connection on one table's changes, `*` on every table (3-2c-1). */
32
+ export declare const MAX_CHANGE_CHANNELS_PER_TABLE = 4;
33
+ /**
34
+ * The hub's INVALID_CHANNEL_CONFIG reason at `config.postgres_changes[<i>].table`
35
+ * for a join over `MAX_CHANGE_CHANNELS_PER_TABLE` (data-core
36
+ * `JOIN_CONFIG_REASONS.changeChannels`): a cap a leave still owed may hold a
37
+ * place under (channel.ts).
38
+ */
39
+ export declare const CHANGE_CHANNELS_REASON: string;
40
+ /** One frame of the server, its fields checked; anything else is null. */
41
+ export type ServerFrame = {
42
+ topic: string;
43
+ event: string;
44
+ payload: Record<string, unknown>;
45
+ ref: string | null;
46
+ /** The join a channel frame is about (3-2a P11); null on the phoenix topic. */
47
+ joinRef: string | null;
48
+ };
49
+ export declare function readServerFrame(data: unknown): ServerFrame | null;
50
+ /**
51
+ * The limit a size refusal names (data-core `tooLargeReason`: "is larger
52
+ * than N bytes"), or null for any other reason. The hub measures a frame
53
+ * before it reads it, so this refusal comes on the phoenix topic with no
54
+ * `ref` (`parseClientFrame`); the SDK matches it to its frames by size.
55
+ */
56
+ export declare function frameLimitOf(reason: unknown): number | null;
57
+ /**
58
+ * A text's length in UTF-8 bytes, as the hub measures a frame: a copy of
59
+ * data-core's `utf8ByteLength` (values.ts) — a lone surrogate is 3 bytes.
60
+ */
61
+ export declare function utf8Length(text: string): number;
62
+ /** One client frame as text; channel frames carry their join's `join_ref`. */
63
+ export declare function clientFrame(topic: string, event: string, payload: Record<string, unknown>, ref: string | null, joinRef?: string): string;
64
+ /**
65
+ * Why a channel name is not valid, as `INVALID_CHANNEL`'s reason: it
66
+ * finishes "The channel name is not valid: it …". The name is measured in
67
+ * code points and kept as it is — no trimming, no Unicode normalisation —
68
+ * but the characters that make a name look like another (control and
69
+ * bidirectional marks) are refused. A control character is reported before
70
+ * a bidirectional one wherever they stand.
71
+ */
72
+ export declare function channelNameProblem(name: string): string | undefined;
73
+ /**
74
+ * The connection's URL (original spec §6.3): a browser's WebSocket cannot
75
+ * send headers, so the key rides in the query, encoded. Nothing logs it;
76
+ * an error never quotes it (socket.ts).
77
+ */
78
+ export declare function socketUrl(baseUrl: string, apiKey: string): string;
@@ -0,0 +1,96 @@
1
+ import { type NaroError } from "../errors.js";
2
+ import { type ServerFrame } from "./protocol.js";
3
+ import type { RealtimeOptions } from "./types.js";
4
+ export type Reply = {
5
+ ok: true;
6
+ response: Record<string, unknown>;
7
+ } | {
8
+ ok: false;
9
+ error: NaroError;
10
+ lost: boolean;
11
+ };
12
+ export type SocketEvents = {
13
+ /** Open: client.ts sends the identity first, then the joins. */
14
+ opened(): void;
15
+ /** A frame no request waits for (channel frames). */
16
+ frame(frame: ServerFrame): void;
17
+ /** Closed, and coming back after a wait. */
18
+ dropped(error: NaroError, status: "CHANNEL_ERROR" | "TIMED_OUT"): void;
19
+ /** Closed for good, until a channel subscribes again. */
20
+ stopped(error: NaroError): void;
21
+ /** A 4401: the conditional delete; the wait begins when it is done, or after `timeoutMs`. */
22
+ sessionRefused(): Promise<void>;
23
+ };
24
+ export type SocketConfig = {
25
+ baseUrl: string;
26
+ apiKey: string;
27
+ service: boolean;
28
+ options: RealtimeOptions;
29
+ events: SocketEvents;
30
+ };
31
+ /**
32
+ * Whose frame a request is, which sets its turn (`request()`): the app's
33
+ * call (the default), behind the frames waiting and refused when it would
34
+ * not go within `timeoutMs`; a frame the SDK sends of its own accord, before
35
+ * the app's; the identity's `access_token`, the same, but after the app's
36
+ * presence frames still to go.
37
+ */
38
+ export type Turn = "app" | "sdk" | "identity";
39
+ /**
40
+ * Runs a handler: what it throws is thrown again on its own (결정 22), and
41
+ * the caller goes on — a socket mid-close, a timer, a promise's handler.
42
+ */
43
+ export declare function safely(callback: () => void): void;
44
+ export declare class RealtimeSocket {
45
+ #private;
46
+ constructor(config: SocketConfig);
47
+ get generation(): number;
48
+ get timeoutMs(): number;
49
+ isOpen(): boolean;
50
+ nextRef(): string;
51
+ /**
52
+ * Opens unless a connection is open, opening or due — once the wait the
53
+ * last close or stop set is over. After the app's own close the counts
54
+ * start over; after a stop they go on, as after a drop.
55
+ */
56
+ connect(): void;
57
+ /** The last channel went: close for good, with no event. */
58
+ disconnect(): void;
59
+ /**
60
+ * Lets go of the open connection and comes back as after a drop (1006),
61
+ * the channels told: for a connection that holds an identity the app no
62
+ * longer means and cannot change (identity.ts). Nothing unless open.
63
+ */
64
+ reconnect(): void;
65
+ /**
66
+ * The hub answered this connection: the next drop waits from the first
67
+ * step — unless the last close was 4008 and no heartbeat was answered
68
+ * since (`#flooded`).
69
+ */
70
+ served(): void;
71
+ /**
72
+ * A frame, in its `turn` (the app's unless said), and the `phx_reply`
73
+ * with its `ref` within `timeoutMs` of when it went — or, for one over the
74
+ * size limit the hub named on this connection, that refusal at once and
75
+ * nothing sent; for an app's frame but a leave that would not go within
76
+ * `timeoutMs`, CLIENT_FLOODING at once and nothing sent.
77
+ */
78
+ request(topic: string, event: string, payload: Record<string, unknown>, options?: {
79
+ joinRef?: string;
80
+ ref?: string;
81
+ turn?: Turn;
82
+ }): Promise<Reply>;
83
+ /**
84
+ * Runs `go` once a frame the SDK sends of its own accord may go: at once
85
+ * when nothing waits before it and there is room — the warnings of the
86
+ * last minute and the requests in flight fewer than `SDK_WARNING_ROOM`
87
+ * (the app's frames still waiting their turn are not in flight yet, and
88
+ * go after the SDK's), or the warnings fewer and none of the SDK's own
89
+ * unanswered — or else when there is, in the order asked. The
90
+ * frame itself is sent with `turn: "sdk"`. `key` names the sender:
91
+ * asked again before it ran, it keeps its place and runs the newer `go`,
92
+ * which reads its own state again then. Nothing while the connection is
93
+ * not open, and forgotten with it: the sender tries again when it is live.
94
+ */
95
+ whenRoom(key: object, go: () => void): void;
96
+ }
@@ -0,0 +1,89 @@
1
+ import type { NaroError } from "../errors.js";
2
+ /** What `subscribe`'s callback hears (original spec §15). */
3
+ export type RealtimeStatus = "SUBSCRIBED" | "CHANNEL_ERROR" | "TIMED_OUT" | "CLOSED";
4
+ export type RealtimeChannelState = "closed" | "joining" | "joined" | "leaving" | "errored";
5
+ export type RealtimeStatusCallback = (status: RealtimeStatus, error?: NaroError) => void;
6
+ export type RealtimeResult = {
7
+ data: null;
8
+ error: NaroError | null;
9
+ };
10
+ /**
11
+ * What `send()`, `track()` and `untrack()` answer. `lost` is true when the
12
+ * connection closed before the server answered: the frame may have been
13
+ * delivered, or not. So `CLIENT_FLOODING` with `lost: false` was refused
14
+ * here and not sent — send it again later — and with `lost: true` the
15
+ * server closed the connection (4008) and this one may have gone through.
16
+ */
17
+ export type RealtimeSendResult = RealtimeResult & {
18
+ lost: boolean;
19
+ };
20
+ export type RealtimeChannelOptions = {
21
+ config?: {
22
+ private?: boolean;
23
+ broadcast?: {
24
+ self?: boolean;
25
+ ack?: boolean;
26
+ };
27
+ presence?: {
28
+ key?: string;
29
+ enabled?: boolean;
30
+ };
31
+ };
32
+ };
33
+ export type PostgresChangesEvent = "*" | "INSERT" | "UPDATE" | "DELETE";
34
+ export type PostgresChangesFilter<Table extends string = string> = {
35
+ event: PostgresChangesEvent;
36
+ schema?: "public";
37
+ table?: Table | "*";
38
+ filter?: string;
39
+ };
40
+ export type RealtimePostgresChangesPayload<Row extends object = Record<string, unknown>> = {
41
+ schema: "public";
42
+ table: string;
43
+ commit_timestamp: string;
44
+ eventType: "INSERT" | "UPDATE" | "DELETE";
45
+ new: Row | Record<string, never>;
46
+ old: Partial<Row>;
47
+ errors: string[] | null;
48
+ };
49
+ export type RealtimeBroadcastMessage = {
50
+ type: "broadcast";
51
+ event: string;
52
+ payload: Record<string, unknown>;
53
+ };
54
+ /** BULK_CHANGE, EVENTS_DROPPED and the reason a channel closed (original spec §7.4). */
55
+ export type RealtimeSystemMessage = {
56
+ status: "ok" | "error";
57
+ extension: string;
58
+ code: string;
59
+ message: string;
60
+ channel: string | null;
61
+ };
62
+ export type RealtimePresenceEntry = Record<string, unknown> & {
63
+ presence_ref: string;
64
+ /** The signed-in user who tracked it, or null: the one value the server sets. */
65
+ naro_user_id: string | null;
66
+ };
67
+ export type RealtimePresenceState = Record<string, RealtimePresenceEntry[]>;
68
+ /** A presence join as supabase-js's listeners hear it. */
69
+ export type RealtimePresenceJoinPayload = {
70
+ event: "join";
71
+ key: string;
72
+ newPresences: RealtimePresenceEntry[];
73
+ /** What the key held before this join, as supabase-js says it; [] for its first. */
74
+ currentPresences: RealtimePresenceEntry[];
75
+ };
76
+ export type RealtimePresenceLeavePayload = {
77
+ event: "leave";
78
+ key: string;
79
+ leftPresences: RealtimePresenceEntry[];
80
+ /** What the key holds after this leave; [] when it was the last. */
81
+ currentPresences: RealtimePresenceEntry[];
82
+ };
83
+ /** The global WebSocket, or `ws`'s: any class built from a URL. */
84
+ export type WebSocketConstructor = new (url: string) => unknown;
85
+ export type RealtimeOptions = {
86
+ WebSocket?: WebSocketConstructor;
87
+ heartbeatIntervalMs?: number;
88
+ timeoutMs?: number;
89
+ };
@@ -1,7 +1,7 @@
1
1
  import type { AuthClient } from "../auth/client.js";
2
2
  import { type HttpConfig } from "../http.js";
3
3
  import type { NaroResponse } from "../types.js";
4
- import type { CreateSignedUploadUrlOptions, DownloadOption, FileObject, FolderEntry, ListOptions, SignedUploadResult, SignedUploadUrl, SignedUrlEntry, UpdateOptions, UploadBody, UploadOptions, UploadResult } from "./types.js";
4
+ import type { CopyOptions, CreateSignedUploadUrlOptions, DownloadOption, FileObject, FolderEntry, ListOptions, MoveOptions, MoveResult, SignedUploadResult, SignedUploadUrl, SignedUrlEntry, UpdateOptions, UploadBody, UploadOptions, UploadResult } from "./types.js";
5
5
  /** One bucket's objects: supabase-js's StorageFileApi. */
6
6
  export declare class StorageFileApi {
7
7
  #private;
@@ -40,9 +40,33 @@ export declare class StorageFileApi {
40
40
  list(prefix?: string, options?: ListOptions): Promise<NaroResponse<(FileObject | FolderEntry)[]>>;
41
41
  /** Removes the objects named — 1 to 1,000 — and answers the ones it removed. */
42
42
  remove(paths: string[]): Promise<NaroResponse<FileObject[]>>;
43
+ /**
44
+ * Renames an object inside its bucket (Storage 3-1i): the same object — its
45
+ * id and owner — under `toPath`, its old name gone. It never replaces an
46
+ * object that is there (`OBJECT_EXISTS`), takes files up to 5 GiB − 5 MiB
47
+ * (`OBJECT_TOO_LARGE`), and needs room in the project for a second copy
48
+ * until it is done (`STORAGE_QUOTA_EXCEEDED`). Its signed links stop
49
+ * working at the old name: a link is bound to the name, the object's id
50
+ * and its bytes, so moving it back can open them again. A backend whose
51
+ * Storage was turned on before this method came answers `NOT_FOUND`:
52
+ * update it with `naro backend enable`.
53
+ */
54
+ move(fromPath: string, toPath: string, options?: MoveOptions): Promise<NaroResponse<MoveResult>>;
55
+ /**
56
+ * Copies an object inside its bucket (Storage 3-1i): the source's bytes,
57
+ * type and cache-control under `toPath` — a new object of the caller's,
58
+ * or with `upsert: true` the one that is there, which keeps its owner.
59
+ * Files up to 5 GiB − 5 MiB, within the bucket's limits as they are now.
60
+ * It answers `{ id, path, fullPath }` — `path` is the name inside the
61
+ * bucket; supabase-js's copy puts the bucket in front of it. A backend
62
+ * whose Storage was turned on before this method came answers
63
+ * `NOT_FOUND`: update it with `naro backend enable`.
64
+ */
65
+ copy(fromPath: string, toPath: string, options?: CopyOptions): Promise<NaroResponse<UploadResult>>;
43
66
  /**
44
67
  * A link to the object for `expiresIn` seconds (1–604,800). It stops
45
- * working once the object is replaced or removed.
68
+ * working once the object is replaced or removed, and while the object is
69
+ * moved away from `path` (moved back, it can open it again).
46
70
  */
47
71
  createSignedUrl(path: string, expiresIn: number, options?: DownloadOption): Promise<NaroResponse<{
48
72
  signedUrl: string;
@@ -115,3 +115,22 @@ export type SignedUrlEntry = {
115
115
  signedUrl: string | null;
116
116
  error: string | null;
117
117
  };
118
+ /** `move`'s options (Storage 3-1i): supabase-js's. */
119
+ export type MoveOptions = {
120
+ /**
121
+ * Taken as supabase-js sends it. Objects move within one bucket: another
122
+ * bucket answers `INVALID_REQUEST` (`details.path` "destinationBucket").
123
+ */
124
+ destinationBucket?: string;
125
+ };
126
+ /** `copy`'s options (Storage 3-1i). */
127
+ export type CopyOptions = {
128
+ /** As `MoveOptions`'s: one bucket only. */
129
+ destinationBucket?: string;
130
+ /** Replace an object at the destination. Default false: `OBJECT_EXISTS`. */
131
+ upsert?: boolean;
132
+ };
133
+ /** What `move` answers: supabase-js's words. */
134
+ export type MoveResult = {
135
+ message: string;
136
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naro-js",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Auth, data and files for your Naro project, from the browser or the server.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -23,8 +23,8 @@
23
23
  "tsup": "8.5.0",
24
24
  "typescript": "7.0.2",
25
25
  "vitest": "3.2.7",
26
- "@next-deploy/config": "0.1.0",
27
26
  "@next-deploy/backend-worker": "0.1.0",
27
+ "@next-deploy/config": "0.1.0",
28
28
  "@next-deploy/data-core": "0.1.0",
29
29
  "@next-deploy/sdk": "0.1.0"
30
30
  },