@towns-protocol/sdk
Version:
For more details, visit the following resources:
308 lines • 12.6 kB
JavaScript
import { DmChannelSettingValue, GdmChannelSettingValue, SpaceChannelSettingValue, StartAuthenticationResponseSchema, FinishAuthenticationResponseSchema, GetSettingsResponseSchema, DmChannelSettingSchema, GdmChannelSettingSchema, SpaceSettingSchema, SpaceChannelSettingSchema, } from '@towns-protocol/proto';
import { create, fromBinary, toBinary, toJson } from '@bufbuild/protobuf';
import { bin_fromBase64, bin_toBase64, dlogger } from '@towns-protocol/dlog';
import { cloneDeep } from 'lodash-es';
import { Observable } from './observable/observable';
import { makeNotificationRpcClient } from './makeNotificationRpcClient';
import { NotificationService } from './notificationService';
import { spaceIdFromChannelId, streamIdAsBytes, streamIdAsString, userIdFromAddress } from './id';
const logger = dlogger('csb:notifications');
class InMemoryNotificationStore {
store = {};
getItem(schema, key) {
const data = this.store[`${schema.typeName}-${key}`];
return data ? fromBinary(schema, bin_fromBase64(data)) : undefined;
}
setItem(schema, key, value) {
this.store[`${schema.typeName}-${key}`] = bin_toBase64(toBinary(schema, value));
}
}
export class NotificationsClient {
signerContext;
url;
store;
opts;
data;
_client;
startResponseKey;
finishResponseKey;
settingsKey;
getClientPromise;
getSettingsPromise;
constructor(signerContext, url, store = new InMemoryNotificationStore(), opts = undefined) {
this.signerContext = signerContext;
this.url = url;
this.store = store;
this.opts = opts;
this.startResponseKey = `startResponse`;
this.finishResponseKey = `finishResponse`;
this.settingsKey = `settings`;
this.data = new Observable({
fetchedAtMs: undefined,
settings: this.getLocalSettings(),
error: undefined,
});
}
get userId() {
return userIdFromAddress(this.signerContext.creatorAddress);
}
getLocalStartResponse() {
return this.store.getItem(StartAuthenticationResponseSchema, this.startResponseKey);
}
setLocalStartResponse(response) {
this.store.setItem(StartAuthenticationResponseSchema, this.startResponseKey, response);
}
getLocalFinishResponse() {
return this.store.getItem(FinishAuthenticationResponseSchema, this.finishResponseKey);
}
setLocalFinishResponse(response) {
this.store.setItem(FinishAuthenticationResponseSchema, this.finishResponseKey, response);
}
getLocalSettings() {
return this.store.getItem(GetSettingsResponseSchema, this.settingsKey);
}
setLocalSettings(settings) {
this.store.setItem(GetSettingsResponseSchema, this.settingsKey, settings);
}
updateLocalSettings(fn) {
if (!this.data.value.settings) {
throw new Error('TNS PUSH: settings has not been fetched');
}
const newSettings = cloneDeep(this.data.value.settings);
fn(newSettings);
this.setLocalSettings(newSettings);
this.data.setValue({ ...this.data.value, settings: newSettings });
}
async getClient() {
if (this.getClientPromise) {
return this.getClientPromise;
}
try {
this.getClientPromise = this._getClient();
const result = await this.getClientPromise;
this.getClientPromise = undefined;
return result;
}
catch (error) {
this.data.setValue({ ...this.data.value, error: error });
this.getClientPromise = undefined;
return undefined;
}
}
async _getClient() {
const startResponse = this.getLocalStartResponse();
const finishResponse = this.getLocalFinishResponse();
// if we have a valid start response and finish response, and the start response is still valid,
// we can re-create the client from the local storage
// and cache it locally to leverage http2 connection pooling
if (startResponse &&
finishResponse &&
startResponse.expiration &&
startResponse.expiration.seconds > Date.now() / 1000) {
if (this._client) {
// if everything is still valid, and we have the client, return the client
return this._client;
}
try {
const client = makeNotificationRpcClient(this.url, finishResponse.sessionToken, this.opts);
this._client = client;
return client;
}
catch (error) {
logger.error('TNS PUSH: error authenticating from local storage, will try from scratch', error);
}
}
// if we don't have a valid client, or if the client has expired,we need to authenticate from scratch
const service = await NotificationService.authenticate(this.signerContext, this.url, this.opts);
this.setLocalStartResponse(service.startResponse);
this.setLocalFinishResponse(service.finishResponse);
return service.notificationRpcClient;
}
async withClient(fn) {
const client = await this.getClient();
if (client) {
try {
return await fn(client);
}
catch (error) {
this.data.setValue({ ...this.data.value, error: error });
throw error;
}
}
// either an error is already logged or the initialize threw an error
return undefined;
}
async getSettings() {
if (this.getSettingsPromise) {
return this.getSettingsPromise;
}
this.getSettingsPromise = this._getSettings();
return this.getSettingsPromise;
}
async _getSettings() {
return this.withClient(async (client) => {
try {
const response = await client.getSettings({});
this.setLocalSettings(response);
this.data.setValue({
fetchedAtMs: Date.now(),
settings: response,
error: undefined,
});
logger.log('TNS PUSH: fetched settings', toJson(GetSettingsResponseSchema, response));
this.getSettingsPromise = undefined;
return response;
}
catch (error) {
this.data.setValue({
...this.data.value,
error: error,
});
throw error;
}
});
}
async subscribeWebPush(subscription) {
return this.withClient(async (client) => {
logger.log('TNS PUSH: subscribing to web push');
return client.subscribeWebPush({ subscription });
});
}
async unsubscribeWebPush(subscription) {
return this.withClient(async (client) => {
logger.log('TNS PUSH: unsubscribing to web push');
return client.unsubscribeWebPush({ subscription });
});
}
async setDmGlobalSetting(value) {
return this.withClient(async (client) => {
await client.setDmGdmSettings({
dmGlobal: value,
gdmGlobal: this.data.value.settings?.gdmGlobal ?? GdmChannelSettingValue.GDM_UNSPECIFIED,
});
this.updateLocalSettings((settings) => {
settings.dmGlobal = value;
});
});
}
async setGdmGlobalSetting(value) {
return this.withClient(async (client) => {
await client.setDmGdmSettings({
dmGlobal: this.data.value.settings?.dmGlobal ?? DmChannelSettingValue.DM_UNSPECIFIED,
gdmGlobal: value,
});
this.updateLocalSettings((settings) => {
settings.gdmGlobal = value;
});
});
}
async setDmChannelSetting(channelId, value) {
return this.withClient(async (client) => {
await client.setDmChannelSetting({
dmChannelId: streamIdAsBytes(channelId),
value,
});
this.updateLocalSettings((settings) => {
settings.dmChannels = settings.dmChannels.filter((c) => streamIdAsString(c.channelId) !== channelId);
settings.dmChannels.push(create(DmChannelSettingSchema, {
channelId: streamIdAsBytes(channelId),
value,
}));
});
});
}
async setGdmChannelSetting(channelId, value) {
return this.withClient(async (client) => {
await client.setGdmChannelSetting({
gdmChannelId: streamIdAsBytes(channelId),
value,
});
this.updateLocalSettings((settings) => {
settings.gdmChannels = settings.gdmChannels.filter((c) => streamIdAsString(c.channelId) !== channelId);
settings.gdmChannels.push(create(GdmChannelSettingSchema, {
channelId: streamIdAsBytes(channelId),
value,
}));
});
});
}
async setSpaceSetting(spaceId, value) {
return this.withClient(async (client) => {
await client.setSpaceSettings({ spaceId: streamIdAsBytes(spaceId), value });
this.updateLocalSettings((settings) => {
const spaceIndex = settings.space.findIndex((s) => streamIdAsString(s.spaceId) === spaceId);
if (spaceIndex != -1) {
settings.space[spaceIndex].value = value;
}
else {
settings.space.push(create(SpaceSettingSchema, {
spaceId: streamIdAsBytes(spaceId),
value,
channels: [],
}));
}
});
});
}
async setChannelSetting(channelId, value) {
const spaceId = spaceIdFromChannelId(channelId);
return this.withClient(async (client) => {
await client.setSpaceChannelSettings({
spaceId: streamIdAsBytes(spaceId),
channelId: streamIdAsBytes(channelId),
value,
});
this.updateLocalSettings((settings) => {
let spaceIndex = settings.space.findIndex((s) => streamIdAsString(s.spaceId) === spaceId);
if (spaceIndex == -1) {
spaceIndex = settings.space.length;
settings.space.push(create(SpaceSettingSchema, {
spaceId: streamIdAsBytes(spaceId),
value: SpaceChannelSettingValue.SPACE_CHANNEL_SETTING_UNSPECIFIED,
channels: [],
}));
}
const channelIndex = settings.space[spaceIndex].channels.findIndex((c) => streamIdAsString(c.channelId) === channelId);
if (channelIndex == -1) {
settings.space[spaceIndex].channels.push(create(SpaceChannelSettingSchema, {
channelId: streamIdAsBytes(channelId),
value,
}));
}
else {
settings.space[spaceIndex].channels[channelIndex].value = value;
}
});
});
}
}
export function getMutedChannelIds(settings) {
if (!settings) {
return undefined;
}
const ids = new Set();
for (const spaceSetting of settings.space) {
if (spaceSetting.value ===
SpaceChannelSettingValue.SPACE_CHANNEL_SETTING_NO_MESSAGES_AND_MUTE) {
ids.add(streamIdAsString(spaceSetting.spaceId));
}
for (const channelSetting of spaceSetting.channels) {
if (channelSetting.value ===
SpaceChannelSettingValue.SPACE_CHANNEL_SETTING_NO_MESSAGES_AND_MUTE) {
ids.add(streamIdAsString(channelSetting.channelId));
}
}
}
for (const dmSetting of settings.dmChannels) {
if (dmSetting.value === DmChannelSettingValue.DM_MESSAGES_NO_AND_MUTE) {
ids.add(streamIdAsString(dmSetting.channelId));
}
}
for (const gdmSetting of settings.gdmChannels) {
if (gdmSetting.value === GdmChannelSettingValue.GDM_MESSAGES_NO_AND_MUTE) {
ids.add(streamIdAsString(gdmSetting.channelId));
}
}
return ids;
}
//# sourceMappingURL=notificationsClient.js.map