Skip to content
Merged
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
39 changes: 4 additions & 35 deletions ably.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2314,30 +2314,24 @@ export declare interface RealtimeObject {
/**
* Retrieves a {@link PathObject} for the object on a channel.
*
* A type parameter can be provided to describe the structure of the Objects on the channel. By default, it uses types from the globally defined `AblyObjectsTypes` interface.
*
* You can specify custom types for Objects by defining a global `AblyObjectsTypes` interface with an `object` property that conforms to Record<string, {@link Value}> type.
* A type parameter can be provided to describe the structure of the Objects on the channel.
*
* Example:
*
* ```typescript
* import { LiveCounter } from 'ably/objects';
* import { LiveCounter } from 'ably';
*
* type MyObject = {
* myTypedCounter: LiveCounter;
* };
*
* declare global {
* export interface AblyObjectsTypes {
* object: MyObject;
* }
* }
* const myTypedObject = await channel.object.get<MyObject>();
* ```
*
* @returns A promise which, upon success, will be fulfilled with a {@link PathObject}. Upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error.
* @experimental
*/
get<T extends Record<string, Value> = AblyDefaultObject>(): Promise<PathObject<LiveMap<T>>>;
get<T extends Record<string, Value>>(): Promise<PathObject<LiveMap<T>>>;

/**
* Registers the provided listener for the specified event. If `on()` is called more than once with the same listener and event, the listener is added multiple times to its listener registry. Therefore, as an example, assuming the same listener is registered twice using `on()`, and an event is emitted once, the listener would be invoked twice.
Expand Down Expand Up @@ -2468,31 +2462,6 @@ export type CompactedValue<T extends Value> =
? T
: any;

declare global {
/**
* A globally defined interface that allows users to define custom types for Objects.
*/
export interface AblyObjectsTypes {
[key: string]: unknown;
}
}

/**
* The default type for the channel object return from the {@link RealtimeObject.get}, based on the globally defined {@link AblyObjectsTypes} interface.
*
* - If no custom types are provided in `AblyObjectsTypes`, defaults to an untyped map representation using the Record<string, {@link Value}> type.
* - If an `object` key exists in `AblyObjectsTypes` and its type conforms to the Record<string, {@link Value}>, it is used as the type for the object returned from the {@link RealtimeObject.get}.
* - If the provided type in `object` key does not match Record<string, {@link Value}>, a type error message is returned.
*/
export type AblyDefaultObject =
// we need a way to know when no types were provided by the user.
// we expect an "object" property to be set on AblyObjectsTypes interface, e.g. it won't be "unknown" anymore
unknown extends AblyObjectsTypes['object']
? Record<string, Value> // no custom types provided; use the default untyped map representation for the entrypoint map
: AblyObjectsTypes['object'] extends Record<string, Value>
? AblyObjectsTypes['object'] // "object" property exists, and it is of an expected type, we can use this interface for the entrypoint map
: `Provided type definition for the channel \`object\` in AblyObjectsTypes is not of an expected Record<string, Value> type`;

/**
* PathObjectBase defines the set of common methods on a PathObject
* that are present regardless of the underlying type.
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/objects/realtimeobject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class RealtimeObject {
* A user can provide an explicit type for the this method to explicitly set the type structure on this particular channel.
* This is useful when working with multiple channels with different underlying data structure.
*/
async get<T extends Record<string, API.Value> = API.AblyDefaultObject>(): Promise<API.PathObject<API.LiveMap<T>>> {
async get<T extends Record<string, API.Value>>(): Promise<API.PathObject<API.LiveMap<T>>> {
this.throwIfInvalidAccessApiConfiguration(); // RTO1a, RTO1b

// if we're not synced yet, wait for sync sequence to finish before returning root
Expand Down
27 changes: 6 additions & 21 deletions test/package/browser/template/src/index-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,27 @@ type MyCustomObject = {
counterKey: LiveCounter;
};

declare global {
export interface AblyObjectsTypes {
object: MyCustomObject;
}
}

type ExplicitObjectType = {
someOtherKey: string;
};

globalThis.testAblyPackage = async function () {
const key = await createSandboxAblyAPIKey();

const realtime = new Ably.Realtime({ key, endpoint: 'nonprod:sandbox', plugins: { Objects } });

const channel = realtime.channels.get('channel', { modes: ['OBJECT_SUBSCRIBE', 'OBJECT_PUBLISH'] });
await channel.attach();
// check Objects can be accessed.
// expect entrypoint to be a PathObject for a LiveMap instance with Object type defined via the global AblyObjectsTypes interface.
// also checks that we can refer to the Objects types exported from 'ably'.
const myObject: Ably.PathObject<LiveMap<MyCustomObject>> = await channel.object.get();
// check Objects can be accessed on a channel with a custom type parameter.
// check that we can refer to the Objects types exported from 'ably' by referencing a LiveMap interface.
const myObject: Ably.PathObject<LiveMap<MyCustomObject>> = await channel.object.get<MyCustomObject>();

// check entrypoint has expected LiveMap TypeScript type methods
const size: number | undefined = myObject.size();

// check custom user provided typings via AblyObjectsTypes are working:
// check custom user provided typings work:
// primitives:
const aNumber: number | undefined = myObject.get('numberKey').value();
const aString: string | undefined = myObject.get('stringKey').value();
const aBoolean: boolean | undefined = myObject.get('booleanKey').value();
const userProvidedUndefined: string | undefined = myObject.get('couldBeUndefined').value();
// objects on the entrypoint:
// objects:
const counter: Ably.LiveCounterPathObject = myObject.get('counterKey');
const map: Ably.LiveMapPathObject<MyCustomObject['mapKey']> = myObject.get('mapKey');
// check string literal types works
Expand All @@ -70,9 +60,4 @@ globalThis.testAblyPackage = async function () {
const typedMessage: Ably.ObjectMessage | undefined = message;
});
unsubscribe();

// check can provide custom types for the object.get() method, ignoring global AblyObjectsTypes interface
const explicitObjectType: Ably.PathObject<LiveMap<ExplicitObjectType>> =
await channel.object.get<ExplicitObjectType>();
const someOtherKey: string | undefined = explicitObjectType.get('someOtherKey').value();
};
3 changes: 1 addition & 2 deletions typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,5 @@
"TypeAlias",
"Variable",
"Namespace"
],
"intentionallyNotExported": ["__global.AblyObjectsTypes"]
]
}
Loading