From 965279f9bba9aaaa128c31b74ef5c0116124fc98 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Fri, 14 Jun 2024 10:43:55 -0500 Subject: [PATCH] Decouple content types from the JS SDK --- packages/js-sdk/package.json | 2 + packages/js-sdk/src/Client.ts | 12 +-- packages/js-sdk/src/Message.ts | 2 +- packages/js-sdk/src/MessageContent.ts | 75 ------------------- packages/js-sdk/src/codecs/Composite.ts | 4 +- packages/js-sdk/src/codecs/Text.ts | 50 ------------- .../js-sdk/src/conversations/Conversation.ts | 2 +- packages/js-sdk/src/index.ts | 7 -- packages/js-sdk/src/types/client.ts | 2 +- packages/js-sdk/test/Client.test.ts | 2 +- packages/js-sdk/test/Compression.test.ts | 2 +- packages/js-sdk/test/ContentTypeTestKey.ts | 6 +- packages/js-sdk/test/Message.test.ts | 2 +- packages/js-sdk/test/MessageContent.test.ts | 55 -------------- packages/js-sdk/test/codecs/Composite.test.ts | 2 +- packages/js-sdk/test/codecs/Text.test.ts | 57 -------------- .../test/conversations/Conversation.test.ts | 4 +- packages/js-sdk/test/helpers.ts | 4 +- yarn.lock | 11 +++ 19 files changed, 35 insertions(+), 266 deletions(-) delete mode 100644 packages/js-sdk/src/MessageContent.ts delete mode 100644 packages/js-sdk/src/codecs/Text.ts delete mode 100644 packages/js-sdk/test/MessageContent.test.ts delete mode 100644 packages/js-sdk/test/codecs/Text.test.ts diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index c0faa9f16..332266756 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -99,6 +99,8 @@ "dependencies": { "@noble/secp256k1": "1.7.1", "@xmtp/consent-proof-signature": "^0.1.3", + "@xmtp/content-type-primitives": "^1.0.1", + "@xmtp/content-type-text": "^1.0.0", "@xmtp/proto": "3.54.0", "@xmtp/user-preferences-bindings-wasm": "^0.3.6", "async-mutex": "^0.5.0", diff --git a/packages/js-sdk/src/Client.ts b/packages/js-sdk/src/Client.ts index 1f15a6066..907e09b9d 100644 --- a/packages/js-sdk/src/Client.ts +++ b/packages/js-sdk/src/Client.ts @@ -1,3 +1,9 @@ +import { + ContentTypeId, + type ContentCodec, + type EncodedContent, +} from '@xmtp/content-type-primitives' +import { ContentTypeText, TextCodec } from '@xmtp/content-type-text' import { messageApi, content as proto } from '@xmtp/proto' import { getAddress, type WalletClient } from 'viem' import KeystoreAuthenticator from '@/authn/KeystoreAuthenticator' @@ -29,7 +35,6 @@ import HttpApiClient, { type ApiClient, type PublishParams, } from './ApiClient' -import { ContentTypeText, TextCodec } from './codecs/Text' import { compress, decompress } from './Compression' import { decodeContactBundle, encodeContactBundle } from './ContactBundle' import { Contacts } from './Contacts' @@ -40,11 +45,6 @@ import { hasMetamaskWithSnaps } from './keystore/snapHelpers' import type BackupClient from './message-backup/BackupClient' import { BackupType } from './message-backup/BackupClient' import { createBackupClient } from './message-backup/BackupClientFactory' -import { - ContentTypeId, - type ContentCodec, - type EncodedContent, -} from './MessageContent' import { packageName, version } from './snapInfo.json' import type { ExtractDecodedType } from './types/client' import type { Signer } from './types/Signer' diff --git a/packages/js-sdk/src/Message.ts b/packages/js-sdk/src/Message.ts index e5179bbde..e92390d20 100644 --- a/packages/js-sdk/src/Message.ts +++ b/packages/js-sdk/src/Message.ts @@ -1,3 +1,4 @@ +import type { ContentTypeId } from '@xmtp/content-type-primitives' import { message as proto, type conversationReference } from '@xmtp/proto' import Long from 'long' import { PublicKey } from '@/crypto/PublicKey' @@ -12,7 +13,6 @@ import Ciphertext from './crypto/Ciphertext' import { sha256 } from './crypto/encryption' import { bytesToHex } from './crypto/utils' import type { KeystoreInterfaces } from './keystore/rpcDefinitions' -import type { ContentTypeId } from './MessageContent' import { dateToNs, nsToDate } from './utils/date' import { buildDecryptV1Request, getResultOrThrow } from './utils/keystore' diff --git a/packages/js-sdk/src/MessageContent.ts b/packages/js-sdk/src/MessageContent.ts deleted file mode 100644 index 8ffb63e44..000000000 --- a/packages/js-sdk/src/MessageContent.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { content as proto } from '@xmtp/proto' - -// Represents proto.ContentTypeId -export class ContentTypeId { - authorityId: string - typeId: string - versionMajor: number - versionMinor: number - - constructor(obj: proto.ContentTypeId) { - this.authorityId = obj.authorityId - this.typeId = obj.typeId - this.versionMajor = obj.versionMajor - this.versionMinor = obj.versionMinor - } - - toString(): string { - return `${this.authorityId}/${this.typeId}:${this.versionMajor}.${this.versionMinor}` - } - - static fromString(contentTypeString: string): ContentTypeId { - const [idString, versionString] = contentTypeString.split(':') - const [authorityId, typeId] = idString.split('/') - const [major, minor] = versionString.split('.') - return new ContentTypeId({ - authorityId, - typeId, - versionMajor: Number(major), - versionMinor: Number(minor), - }) - } - - sameAs(id: ContentTypeId): boolean { - return this.authorityId === id.authorityId && this.typeId === id.typeId - } -} - -// Represents proto.EncodedContent -export interface EncodedContent> { - type: ContentTypeId - parameters: Parameters - fallback?: string - compression?: number - content: Uint8Array -} - -// Define an interface for the encoding machinery for a specific content type -// associated with a given ContentTypeId -// A codec can be registered with a Client to be automatically invoked when -// handling content of the corresponding content type. -export interface CodecRegistry { - // eslint-disable-next-line no-use-before-define, @typescript-eslint/no-explicit-any - codecFor(contentType: ContentTypeId): ContentCodec | undefined -} - -export interface ContentCodec { - contentType: ContentTypeId - encode(content: T, registry: CodecRegistry): EncodedContent - decode(content: EncodedContent, registry: CodecRegistry): T - fallback(content: T): string | undefined - shouldPush: (content: T) => boolean -} - -// xmtp.org/fallback -// -// This is not a real content type, it is used to signal to the recipient -// that the content in the message is the fallback description (if present) -// in case the original content type is not supported. -// This content type MUST NOT be used to send content. -export const ContentTypeFallback = new ContentTypeId({ - authorityId: 'xmtp.org', - typeId: 'fallback', - versionMajor: 1, - versionMinor: 0, -}) diff --git a/packages/js-sdk/src/codecs/Composite.ts b/packages/js-sdk/src/codecs/Composite.ts index e5a115e6f..2bbe9f5d6 100644 --- a/packages/js-sdk/src/codecs/Composite.ts +++ b/packages/js-sdk/src/codecs/Composite.ts @@ -1,10 +1,10 @@ -import { composite as proto } from '@xmtp/proto' import { ContentTypeId, type CodecRegistry, type ContentCodec, type EncodedContent, -} from '@/MessageContent' +} from '@xmtp/content-type-primitives' +import { composite as proto } from '@xmtp/proto' // xmtp.org/composite // diff --git a/packages/js-sdk/src/codecs/Text.ts b/packages/js-sdk/src/codecs/Text.ts deleted file mode 100644 index af1ecb461..000000000 --- a/packages/js-sdk/src/codecs/Text.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - ContentTypeId, - type ContentCodec, - type EncodedContent, -} from '@/MessageContent' - -// xmtp.org/text -// -// This content type is used for a plain text content represented by a simple string -export const ContentTypeText = new ContentTypeId({ - authorityId: 'xmtp.org', - typeId: 'text', - versionMajor: 1, - versionMinor: 0, -}) - -export enum Encoding { - utf8 = 'UTF-8', -} - -export class TextCodec implements ContentCodec { - get contentType(): ContentTypeId { - return ContentTypeText - } - - encode(content: string): EncodedContent { - return { - type: ContentTypeText, - parameters: { encoding: Encoding.utf8 }, - content: new TextEncoder().encode(content), - } - } - - decode(content: EncodedContent): string { - const encoding = content.parameters.encoding - if (encoding && encoding !== Encoding.utf8) { - throw new Error(`unrecognized encoding ${encoding}`) - } - return new TextDecoder().decode(content.content) - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - fallback(content: string): string | undefined { - return undefined - } - - shouldPush() { - return true - } -} diff --git a/packages/js-sdk/src/conversations/Conversation.ts b/packages/js-sdk/src/conversations/Conversation.ts index 340c3f542..a6bc7655a 100644 --- a/packages/js-sdk/src/conversations/Conversation.ts +++ b/packages/js-sdk/src/conversations/Conversation.ts @@ -1,3 +1,4 @@ +import { ContentTypeText } from '@xmtp/content-type-text' import { message, content as proto, @@ -13,7 +14,6 @@ import type { SendOptions, } from '@/Client' import type Client from '@/Client' -import { ContentTypeText } from '@/codecs/Text' import type { ConsentState } from '@/Contacts' import { sha256 } from '@/crypto/encryption' import { SignedPublicKey } from '@/crypto/PublicKey' diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 4e241590d..45e889a70 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -43,13 +43,6 @@ export { export type { Conversation } from '@/conversations/Conversation' export { ConversationV1, ConversationV2 } from '@/conversations/Conversation' export { default as Conversations } from '@/conversations/Conversations' -export type { - CodecRegistry, - ContentCodec, - EncodedContent, -} from './MessageContent' -export { ContentTypeId, ContentTypeFallback } from './MessageContent' -export { TextCodec, ContentTypeText } from './codecs/Text' export type { Composite } from './codecs/Composite' export { CompositeCodec, ContentTypeComposite } from './codecs/Composite' export type { diff --git a/packages/js-sdk/src/types/client.ts b/packages/js-sdk/src/types/client.ts index a22d30193..8f7dd9af1 100644 --- a/packages/js-sdk/src/types/client.ts +++ b/packages/js-sdk/src/types/client.ts @@ -1,5 +1,5 @@ +import type { ContentCodec } from '@xmtp/content-type-primitives' import type Client from '@/Client' -import type { ContentCodec } from '@/MessageContent' export type GetMessageContentTypeFromClient = C extends Client ? T : never diff --git a/packages/js-sdk/test/Client.test.ts b/packages/js-sdk/test/Client.test.ts index 21c38b40e..01fd6555b 100644 --- a/packages/js-sdk/test/Client.test.ts +++ b/packages/js-sdk/test/Client.test.ts @@ -1,3 +1,4 @@ +import { ContentTypeText, TextCodec } from '@xmtp/content-type-text' import { message } from '@xmtp/proto' import type { PublishResponse } from '@xmtp/proto/ts/dist/types/message_api/v1/message_api.pb' import { Wallet } from 'ethers' @@ -8,7 +9,6 @@ import { assert, vi } from 'vitest' import HttpApiClient, { ApiUrls } from '@/ApiClient' import Client, { Compression, type ClientOptions } from '@/Client' import { CompositeCodec } from '@/codecs/Composite' -import { ContentTypeText, TextCodec } from '@/codecs/Text' import { PrivateKey } from '@/crypto/PrivateKey' import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' diff --git a/packages/js-sdk/test/Compression.test.ts b/packages/js-sdk/test/Compression.test.ts index e4da51914..00352be09 100644 --- a/packages/js-sdk/test/Compression.test.ts +++ b/packages/js-sdk/test/Compression.test.ts @@ -1,5 +1,5 @@ +import { ContentTypeText } from '@xmtp/content-type-text' import { content as proto } from '@xmtp/proto' -import { ContentTypeText } from '@/codecs/Text' import { compress, decompress, diff --git a/packages/js-sdk/test/ContentTypeTestKey.ts b/packages/js-sdk/test/ContentTypeTestKey.ts index 16fa87c01..e4d52dc7d 100644 --- a/packages/js-sdk/test/ContentTypeTestKey.ts +++ b/packages/js-sdk/test/ContentTypeTestKey.ts @@ -1,10 +1,10 @@ -import { publicKey } from '@xmtp/proto' -import { PublicKey } from '@/crypto/PublicKey' import { ContentTypeId, type ContentCodec, type EncodedContent, -} from '@/MessageContent' +} from '@xmtp/content-type-primitives' +import { publicKey } from '@xmtp/proto' +import { PublicKey } from '@/crypto/PublicKey' export const ContentTypeTestKey = new ContentTypeId({ authorityId: 'xmtp.test', diff --git a/packages/js-sdk/test/Message.test.ts b/packages/js-sdk/test/Message.test.ts index 99e999d77..b242eb3ce 100644 --- a/packages/js-sdk/test/Message.test.ts +++ b/packages/js-sdk/test/Message.test.ts @@ -1,9 +1,9 @@ +import { ContentTypeText } from '@xmtp/content-type-text' import type { Wallet } from 'ethers' import { createWalletClient, http } from 'viem' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { mainnet } from 'viem/chains' import Client from '@/Client' -import { ContentTypeText } from '@/codecs/Text' import { ConversationV1 } from '@/conversations/Conversation' import { sha256 } from '@/crypto/encryption' import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' diff --git a/packages/js-sdk/test/MessageContent.test.ts b/packages/js-sdk/test/MessageContent.test.ts deleted file mode 100644 index d3b17168c..000000000 --- a/packages/js-sdk/test/MessageContent.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ContentTypeId } from '@/MessageContent' - -describe('ContentTypeId', () => { - it('creates a new content type', () => { - const contentType = new ContentTypeId({ - authorityId: 'foo', - typeId: 'bar', - versionMajor: 1, - versionMinor: 0, - }) - - expect(contentType.authorityId).toEqual('foo') - expect(contentType.typeId).toEqual('bar') - expect(contentType.versionMajor).toEqual(1) - expect(contentType.versionMinor).toEqual(0) - }) - - it('creates a string from a content type', () => { - const contentType = new ContentTypeId({ - authorityId: 'foo', - typeId: 'bar', - versionMajor: 1, - versionMinor: 0, - }) - - expect(contentType.toString()).toEqual('foo/bar:1.0') - }) - - it('creates a content type from a string', () => { - const contentType = ContentTypeId.fromString('foo/bar:1.0') - - expect(contentType.authorityId).toEqual('foo') - expect(contentType.typeId).toEqual('bar') - expect(contentType.versionMajor).toEqual(1) - expect(contentType.versionMinor).toEqual(0) - }) - - it('compares two content types', () => { - const contentType1 = new ContentTypeId({ - authorityId: 'foo', - typeId: 'bar', - versionMajor: 1, - versionMinor: 0, - }) - const contentType2 = new ContentTypeId({ - authorityId: 'baz', - typeId: 'qux', - versionMajor: 1, - versionMinor: 0, - }) - - expect(contentType1.sameAs(contentType2)).toBe(false) - expect(contentType1.sameAs(contentType1)).toBe(true) - }) -}) diff --git a/packages/js-sdk/test/codecs/Composite.test.ts b/packages/js-sdk/test/codecs/Composite.test.ts index 4790e524b..6d8c63572 100644 --- a/packages/js-sdk/test/codecs/Composite.test.ts +++ b/packages/js-sdk/test/codecs/Composite.test.ts @@ -1,5 +1,5 @@ +import { ContentTypeText } from '@xmtp/content-type-text' import { CompositeCodec, ContentTypeComposite } from '@/codecs/Composite' -import { ContentTypeText } from '@/codecs/Text' import { CodecRegistry } from '@test/helpers' describe('CompositeType', () => { diff --git a/packages/js-sdk/test/codecs/Text.test.ts b/packages/js-sdk/test/codecs/Text.test.ts deleted file mode 100644 index daca2fc45..000000000 --- a/packages/js-sdk/test/codecs/Text.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ContentTypeText, Encoding } from '@/codecs/Text' -import { CodecRegistry } from '@test/helpers' - -describe('ContentTypeText', () => { - const codecs = new CodecRegistry() - const codec = codecs.codecFor(ContentTypeText) - expect(codec).toBeTruthy() - - it('can encode/decode text', () => { - const text = 'Hey' - const ec = codec!.encode(text, codecs) - expect(ec.type.sameAs(ContentTypeText)).toBe(true) - expect(ec.parameters.encoding).toEqual(Encoding.utf8) - const text2 = codec!.decode(ec, codecs) - expect(text2).toEqual(text) - }) - - it('defaults to utf-8', () => { - const text = 'Hey' - const ec = codec!.encode(text, codecs) - expect(ec.type.sameAs(ContentTypeText)).toBe(true) - expect(ec.parameters.encoding).toEqual(Encoding.utf8) - delete ec.parameters.encoding - const text2 = codec!.decode(ec, codecs) - expect(text2).toEqual(text) - }) - - it('throws on non-string', () => { - expect(() => - new TextEncoder().encode({ - toString() { - throw new Error('GM!') - }, - } as any) - ).toThrow('GM!') - }) - - it('throws on invalid input', () => { - const ec = { - type: ContentTypeText, - parameters: {}, - content: {} as Uint8Array, - } - expect(() => codec!.decode(ec, codecs)).toThrow() - }) - - it('throws on unknown encoding', () => { - const ec = { - type: ContentTypeText, - parameters: { encoding: 'UTF-16' }, - content: new Uint8Array(0), - } - expect(() => codec!.decode(ec, codecs)).toThrow( - 'unrecognized encoding UTF-16' - ) - }) -}) diff --git a/packages/js-sdk/test/conversations/Conversation.test.ts b/packages/js-sdk/test/conversations/Conversation.test.ts index 41ded24be..db65a3747 100644 --- a/packages/js-sdk/test/conversations/Conversation.test.ts +++ b/packages/js-sdk/test/conversations/Conversation.test.ts @@ -1,14 +1,14 @@ +import { ContentTypeId } from '@xmtp/content-type-primitives' +import { ContentTypeText } from '@xmtp/content-type-text' import { content as proto } from '@xmtp/proto' import { assert, vi } from 'vitest' import { SortDirection } from '@/ApiClient' import type Client from '@/Client' import { Compression } from '@/Client' -import { ContentTypeText } from '@/codecs/Text' import { ConversationV2 } from '@/conversations/Conversation' import { PrivateKey } from '@/crypto/PrivateKey' import { SignedPublicKeyBundle } from '@/crypto/PublicKeyBundle' import { DecodedMessage, MessageV1, type MessageV2 } from '@/Message' -import { ContentTypeId } from '@/MessageContent' import { sleep } from '@/utils/async' import { buildDirectMessageTopic } from '@/utils/topic' import { ContentTypeTestKey, TestKeyCodec } from '@test/ContentTypeTestKey' diff --git a/packages/js-sdk/test/helpers.ts b/packages/js-sdk/test/helpers.ts index 2f22ed1e7..e8254b212 100644 --- a/packages/js-sdk/test/helpers.ts +++ b/packages/js-sdk/test/helpers.ts @@ -1,13 +1,13 @@ +import type { ContentCodec, ContentTypeId } from '@xmtp/content-type-primitives' +import { TextCodec } from '@xmtp/content-type-text' import { fetcher, type messageApi } from '@xmtp/proto' import { Wallet } from 'ethers' import Client, { type ClientOptions } from '@/Client' -import { TextCodec } from '@/codecs/Text' import { PrivateKey } from '@/crypto/PrivateKey' import type { PublicKeyBundle, SignedPublicKeyBundle, } from '@/crypto/PublicKeyBundle' -import type { ContentCodec, ContentTypeId } from '@/MessageContent' import type Stream from '@/Stream' import type { Signer } from '@/types/Signer' import { promiseWithTimeout } from '@/utils/async' diff --git a/yarn.lock b/yarn.lock index 079bfe16c..35cb8a3d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2872,6 +2872,15 @@ __metadata: languageName: node linkType: hard +"@xmtp/content-type-text@npm:^1.0.0": + version: 1.0.0 + resolution: "@xmtp/content-type-text@npm:1.0.0" + dependencies: + "@xmtp/content-type-primitives": "npm:^1.0.1" + checksum: 10/b195060ad5686a6ace2772d5208d535d1f5062820629764aec52cedf3f2630885b5913aea6d2f0186a49139845c20d2ded783c6bf998884f09374c07b183141f + languageName: node + linkType: hard + "@xmtp/mls-client-bindings-node@npm:^0.0.4": version: 0.0.4 resolution: "@xmtp/mls-client-bindings-node@npm:0.0.4" @@ -2992,6 +3001,8 @@ __metadata: "@typescript-eslint/parser": "npm:^7.8.0" "@vitest/coverage-v8": "npm:^1.6.0" "@xmtp/consent-proof-signature": "npm:^0.1.3" + "@xmtp/content-type-primitives": "npm:^1.0.1" + "@xmtp/content-type-text": "npm:^1.0.0" "@xmtp/proto": "npm:3.54.0" "@xmtp/rollup-plugin-resolve-extensions": "npm:1.0.1" "@xmtp/user-preferences-bindings-wasm": "npm:^0.3.6"