Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
86dd319
DX-1209: inline fix-it hints on SDK ErrorInfo throw sites
umair-ably May 26, 2026
f786767
DX-1209: tighten hint language, forecast server walls, add CLI tips
umair-ably May 26, 2026
28460a2
DX-1209: add scripts/hint-coverage.ts + wire into lint CI
umair-ably May 27, 2026
ccb7b6d
DX-1209: address Lint + Bundle CI failures
umair-ably May 27, 2026
9cc0ff8
DX-1209: fix push plugin bugs surfaced by PR #2233 review
umair-ably May 28, 2026
e225978
DX-1209: extend ErrorInfo with values-object constructor overload
umair-ably May 28, 2026
03c056f
DX-1209: migrate hint-bearing throw sites to single-call form
umair-ably May 28, 2026
5bee6d5
DX-1209: tighten hint and message content per PR #2233 review
umair-ably May 28, 2026
690f4d9
DX-1209: style polish across hint text
umair-ably May 28, 2026
ba5fd88
DX-1209: rewrite hint-coverage as a TypeScript AST walker
umair-ably May 28, 2026
a25b545
DX-1209: tighten error-hints test + document bundle threshold bump
umair-ably May 28, 2026
f043eab
DX-1209: trim message/hint redundancy per D1 audit
umair-ably May 28, 2026
9c20e0b
DX-1209: forward inner hint when wrapping decode failures
umair-ably May 28, 2026
98e38d3
DX-1209: broaden device-token hint to cover unsubscribe path
umair-ably May 28, 2026
a17b1cd
DX-1209: drop hint-coverage script and hint-pinning tests
umair-ably May 29, 2026
f0c80f2
DX-1209: second message/hint redundancy pass
umair-ably Jun 3, 2026
35f3492
language tweaks based on PR feedback
umair-ably Jun 24, 2026
b1a159e
DX-1209: fix error hint factual accuracy from PR #2233 review
umair-ably Jun 30, 2026
311d679
DX-1209: fix hint factual accuracy and cross-site consistency from fu…
umair-ably Jul 2, 2026
8c70f4e
DX-1209: promote fact-bearing parentheticals in hints to sentences
umair-ably Jul 3, 2026
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
2 changes: 1 addition & 1 deletion scripts/moduleReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { gzip } from 'zlib';
import Table from 'cli-table';

// The maximum size we allow for a minimal useful Realtime bundle (i.e. one that can subscribe to a channel)
const minimalUsefulRealtimeBundleSizeThresholdsKiB = { raw: 108, gzip: 33 };
const minimalUsefulRealtimeBundleSizeThresholdsKiB = { raw: 118, gzip: 36 };

const baseClientNames = ['BaseRest', 'BaseRealtime'];

Expand Down
206 changes: 147 additions & 59 deletions src/common/lib/client/auth.ts

Large diffs are not rendered by default.

30 changes: 21 additions & 9 deletions src/common/lib/client/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,33 @@ class BaseClient {
if (!keyMatch) {
const msg = 'invalid key parameter';
Logger.logAction(this.logger, Logger.LOG_ERROR, 'BaseClient()', msg);
throw new ErrorInfo(msg, 40400, 404);
throw new ErrorInfo({
message: msg,
code: 40400,
statusCode: 404,
hint: 'ClientOptions.key must be the full "appId.keyId:secret" string copied from the Ably dashboard. If you have the Ably CLI installed, `ably auth keys list` shows the keys configured on the current app.',
});
}
normalOptions.keyName = keyMatch[1];
normalOptions.keySecret = keyMatch[2];
}

if ('clientId' in normalOptions) {
if (!(typeof normalOptions.clientId === 'string' || normalOptions.clientId === null))
throw new ErrorInfo('clientId must be either a string or null', 40012, 400);
else if (normalOptions.clientId === '*')
throw new ErrorInfo(
'Can’t use "*" as a clientId as that string is reserved. (To change the default token request behaviour to use a wildcard clientId, use {defaultTokenParams: {clientId: "*"}})',
40012,
400,
);
if (!(typeof normalOptions.clientId === 'string' || normalOptions.clientId === null)) {
throw new ErrorInfo({
message: 'clientId must be either a string or null',
code: 40012,
statusCode: 400,
hint: 'Pass a stable string such as a user id to identify the client, or null (or omit it) for an anonymous client. Values like numbers or objects are not accepted.',
});
} else if (normalOptions.clientId === '*') {
throw new ErrorInfo({
message: 'Can’t use "*" as a clientId as that string is reserved',
code: 40012,
statusCode: 400,
hint: 'ClientOptions.clientId sets one fixed identity and cannot be "*". To let this client act as any clientId, request a wildcard token instead: set defaultTokenParams: { clientId: "*" } on the client. The "*" belongs in the token request, not in ClientOptions.clientId.',
});
}
}

Logger.logAction(this.logger, Logger.LOG_MINOR, 'BaseClient()', 'started; version = ' + Defaults.version);
Expand Down
12 changes: 7 additions & 5 deletions src/common/lib/client/baserealtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,13 @@ class Channels extends EventEmitter {
channel = this.all[name] = new RealtimeChannel(this.realtime, name, channelOptions);
} else if (channelOptions) {
if (channel._shouldReattachToSetOptions(channelOptions, channel.channelOptions)) {
throw new ErrorInfo(
'Channels.get() cannot be used to set channel options that would cause the channel to reattach. Please, use RealtimeChannel.setOptions() instead.',
40000,
400,
);
throw new ErrorInfo({
message:
'Channels.get() cannot be used to set channel options that would cause the channel to reattach: channels.get() returns the existing channel instance.',
code: 40000,
statusCode: 400,
hint: 'To change params or modes on an existing channel, call channel.setOptions(opts) on the channel returned by channels.get(name). setOptions() re-attaches the channel to apply the new options.',
});
}
channel.setOptions(channelOptions);
}
Expand Down
14 changes: 12 additions & 2 deletions src/common/lib/client/paginatedresource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,25 @@ export class PaginatedResult<T> {
return this.get(this._relParams!.first);
}

throw new ErrorInfo('No link to the first page of results', 40400, 404);
throw new ErrorInfo({
message: 'No link to the first page of results',
code: 40400,
statusCode: 404,
hint: 'first() is only available on results from a paginated REST query (such as channel history, presence, or stats), whose response includes a link to the first page. This result has no such link, so there is no first page to return.',
});
}

async current(): Promise<PaginatedResult<T>> {
if (this.hasCurrent()) {
return this.get(this._relParams!.current);
}

throw new ErrorInfo('No link to the current page of results', 40400, 404);
throw new ErrorInfo({
message: 'No link to the current page of results',
code: 40400,
statusCode: 404,
hint: 'current() reloads the current page and is only available on results from a paginated REST query (such as channel history, presence, or stats). This result has no such link. To page through results, use next() with hasNext() or isLast() instead.',
});
}

async next(): Promise<PaginatedResult<T> | null> {
Expand Down
64 changes: 50 additions & 14 deletions src/common/lib/client/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ import type {
import Platform from 'common/platform';
import type { ErrCallback } from 'common/types/utils';

// Keep this byte-identical to the copy in src/plugins/push/pushactivation.ts. The plugin only
// type-imports from common client modules, so a value import here is not viable for the build.
const PUSH_ACTIVATION_NOT_AVAILABLE_HINT =
'Run push.activate() in a browser environment with service worker support. From a server, use client.push.admin instead. Call client.push.admin.publish(recipient, payload) to send to a device or clientId. Call client.push.admin.deviceRegistrations.save(device) to register a device record.';

const PUSH_DEACTIVATION_NOT_AVAILABLE_HINT =
'Run push.deactivate() in a browser environment with service worker support. From a server, call client.push.admin.deviceRegistrations.remove(deviceId) to remove a device registration.';

class Push {
client: BaseClient;
admin: Admin;
Expand All @@ -37,11 +45,24 @@ class Push {
return;
}
if (!this.stateMachine) {
reject(new ErrorInfo('This platform is not supported as a target of push notifications', 40000, 400));
const err = new ErrorInfo({
message:
'This platform is not supported as a target of push notifications: push activation requires a browser environment with service worker support',
code: 40000,
statusCode: 400,
hint: PUSH_ACTIVATION_NOT_AVAILABLE_HINT,
});
reject(err);
return;
}
if (this.stateMachine.activatedCallback) {
reject(new ErrorInfo('Activation already in progress', 40000, 400));
const err = new ErrorInfo({
message: 'Activation already in progress',
code: 40000,
statusCode: 400,
hint: 'Await the in-flight push.activate() before calling it again.',
});
reject(err);
return;
}
this.stateMachine.activatedCallback = (err: ErrorInfo) => {
Expand All @@ -65,11 +86,24 @@ class Push {
return;
}
if (!this.stateMachine) {
reject(new ErrorInfo('This platform is not supported as a target of push notifications', 40000, 400));
const err = new ErrorInfo({
message:
'This platform is not supported as a target of push notifications: push activation requires a browser environment with service worker support',
code: 40000,
statusCode: 400,
hint: PUSH_DEACTIVATION_NOT_AVAILABLE_HINT,
});
reject(err);
return;
}
if (this.stateMachine.deactivatedCallback) {
reject(new ErrorInfo('Deactivation already in progress', 40000, 400));
const err = new ErrorInfo({
message: 'Deactivation already in progress',
code: 40000,
statusCode: 400,
hint: 'Await the in-flight push.deactivate() before calling it again.',
});
reject(err);
return;
}
this.stateMachine.deactivatedCallback = (err: ErrorInfo) => {
Expand Down Expand Up @@ -156,11 +190,12 @@ class DeviceRegistrations {
deviceId = deviceIdOrDetails.id || deviceIdOrDetails;

if (typeof deviceId !== 'string' || !deviceId.length) {
throw new ErrorInfo(
'First argument to DeviceRegistrations#get must be a deviceId string or DeviceDetails',
40000,
400,
);
throw new ErrorInfo({
message: 'First argument to DeviceRegistrations#get must be a deviceId string or DeviceDetails',
code: 40000,
statusCode: 400,
hint: 'Pass either the device id string or a DeviceDetails object with a non-empty .id field. The local device id is available from client.device().id after push.activate() completes. Alternatively pass the .id of a DeviceDetails returned by push.admin.deviceRegistrations.save().',
});
}

Utils.mixin(headers, client.options.headers);
Expand Down Expand Up @@ -209,11 +244,12 @@ class DeviceRegistrations {
deviceId = deviceIdOrDetails.id || deviceIdOrDetails;

if (typeof deviceId !== 'string' || !deviceId.length) {
throw new ErrorInfo(
'First argument to DeviceRegistrations#remove must be a deviceId string or DeviceDetails',
40000,
400,
);
throw new ErrorInfo({
message: 'First argument to DeviceRegistrations#remove must be a deviceId string or DeviceDetails',
code: 40000,
statusCode: 400,
hint: 'Pass either the device id string or the DeviceDetails object (with a non-empty .id field). To deactivate the local device, call client.push.deactivate() instead.',
});
}

Utils.mixin(headers, client.options.headers);
Expand Down
14 changes: 8 additions & 6 deletions src/common/lib/client/realtimeannotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ class RealtimeAnnotations {
await channel.attach();
}

// explicit check for attach state in caes attachOnSubscribe=false
// explicit check for attach state in case attachOnSubscribe=false
if ((this.channel.state === 'attached' && this.channel._mode & flags.ANNOTATION_SUBSCRIBE) === 0) {
throw new ErrorInfo(
"You are trying to add an annotation listener, but you haven't requested the annotation_subscribe channel mode in ChannelOptions, so this won't do anything (we only deliver annotations to clients who have explicitly requested them)",
93001,
400,
);
throw new ErrorInfo({
message:
"You are trying to add an annotation listener, but you haven't requested the annotation_subscribe channel mode in ChannelOptions, so this won't do anything (we only deliver annotations to clients who have explicitly requested them)",
code: 93001,
statusCode: 400,
hint: 'Enable the mode on the channel with channel.setOptions({ modes: ["subscribe", "annotation_subscribe"] }), which re-attaches with the new mode (calling channels.get(name, { modes }) on an existing channel throws, and appending to channel.modes does not enable it server-side). If the re-attach is rejected by the server, confirm the channel namespace has "Message annotations, updates, appends, and deletes" enabled in the Ably dashboard and that your API key has annotation-subscribe capability on this channel. If you have the Ably CLI installed, `ably apps rules list` shows which namespaces have it enabled and `ably auth keys list` shows the capabilities of your key.',
});
}
}

Expand Down
Loading