diff --git a/.changeset/twenty-eggs-protect.md b/.changeset/twenty-eggs-protect.md new file mode 100644 index 000000000..1cf6499d2 --- /dev/null +++ b/.changeset/twenty-eggs-protect.md @@ -0,0 +1,16 @@ +--- +"@xmtp/xmtp-js": major +--- + +### BREAKING CHANGES + +- Removed internal content types and their primitives +- Added content types and primitives from their respective packages +- Removed `ContentTypeComposite`, see [XIP-19](https://community.xmtp.org/t/xip-19-deprecate-the-composite-codec/525) for more details +- Removed `ContentTypeFallback` + +With this update, the following are no longer exported from the JS SDK: `ContentTypeId`, `CodecRegistry`, `ContentCodec`, `EncodedContent`, `ContentTypeFallback`, `TextCodec`, `ContentTypeText`, `Composite`, `CompositeCodec`, `ContentTypeComposite` + +For content type primitives, use the new `@xmtp/content-type-primitives` package. It exports `ContentTypeId`, `CodecRegistry`, `ContentCodec`, and `EncodedContent`. + +The text content type and codec can now be found at `@xmtp/content-type-text`. It exports `ContentTypeText`, `Encoding`, and `TextCodec`. diff --git a/packages/js-sdk/README.md b/packages/js-sdk/README.md index cd2e4144c..99891b080 100644 --- a/packages/js-sdk/README.md +++ b/packages/js-sdk/README.md @@ -318,7 +318,7 @@ To learn more about content types, see [Content types with XMTP](https://xmtp.or Support for other types of content can be added by registering additional `ContentCodecs` with the `Client`. Every codec is associated with a content type identifier, `ContentTypeId`, which is used to signal to the client which codec should be used to process the content that is being sent or received. -For example, see the [Codecs](https://github.com/xmtp/xmtp-js/tree/main/src/codecs) available in `xmtp-js`. +For examples, see the [content types](https://github.com/xmtp/xmtp-js-content-types/tree/main/packages) available in the `xmtp-js-content-types` repository. If there is a concern that the recipient may not be able to handle a non-standard content type, the sender can use the `contentFallback` option to provide a string that describes the content being sent. If the recipient fails to decode the original content, the fallback will replace it and can be used to inform the recipient what the original content was. @@ -340,13 +340,6 @@ As shown in the example above, you must provide a `contentFallback` value. Use i Additional codecs can be configured through the `ClientOptions` parameter of `Client.create`. The `codecs` option is a list of codec instances that should be added to the default set of codecs (currently only the `TextCodec`). If a codec is added for a content type that is already in the default set, it will replace the original codec. -```ts -// Adding support for `xmtp.org/composite` content type -import { CompositeCodec } from '@xmtp/xmtp-js' - -const xmtp = Client.create(wallet, { codecs: [new CompositeCodec()] }) -``` - To learn more about how to build a custom content type, see [Build a custom content type](https://xmtp.org/docs/content-types/introduction#create-custom-content-types). Custom codecs and content types may be proposed as interoperable standards through XRCs. To learn about the custom content type proposal process, see [XIP-5](https://github.com/xmtp/XIPs/blob/main/XIPs/xip-5-message-content-types.md). 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/rollup.config.js b/packages/js-sdk/rollup.config.js index 6ac3ae4f8..3415d65d6 100644 --- a/packages/js-sdk/rollup.config.js +++ b/packages/js-sdk/rollup.config.js @@ -10,6 +10,8 @@ import tsConfigPaths from 'rollup-plugin-tsconfig-paths' const external = [ '@noble/secp256k1', '@xmtp/consent-proof-signature', + '@xmtp/content-type-text', + '@xmtp/content-type-primitives', '@xmtp/proto', '@xmtp/user-preferences-bindings-wasm', '@xmtp/user-preferences-bindings-wasm/web', 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 deleted file mode 100644 index e5a115e6f..000000000 --- a/packages/js-sdk/src/codecs/Composite.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { composite as proto } from '@xmtp/proto' -import { - ContentTypeId, - type CodecRegistry, - type ContentCodec, - type EncodedContent, -} from '@/MessageContent' - -// xmtp.org/composite -// -// Composite is a generic sequence of multiple parts of arbitrary content type. -// It can be nested arbitrarily (composite of composites). - -export const ContentTypeComposite = new ContentTypeId({ - authorityId: 'xmtp.org', - typeId: 'composite', - versionMajor: 1, - versionMinor: 0, -}) - -// Composite type defines the expected structure of values -// that can be processed by the CompositeCodec -export type Composite = - | { - type: ContentTypeId - // eslint-disable-next-line @typescript-eslint/no-explicit-any - content: any - } - | { parts: Composite[] } - -// CompositeCodec implements encoding/decoding of Composite values. -// Register this codec with the Client if you want support for Composite content. -export class CompositeCodec implements ContentCodec { - get contentType(): ContentTypeId { - return ContentTypeComposite - } - - encode(content: Composite, codecs: CodecRegistry): EncodedContent { - const part = this.toProto(content, codecs) - let composite: proto.Composite - if (part.composite) { - composite = part.composite - } else { - composite = { parts: [part] } - } - const bytes = proto.Composite.encode(composite).finish() - return { - type: ContentTypeComposite, - parameters: {}, - content: bytes, - } - } - - decode(content: EncodedContent, codecs: CodecRegistry): Composite { - return this.fromProto( - { composite: proto.Composite.decode(content.content), part: undefined }, - codecs - ) - } - - private toProto( - content: Composite, - codecs: CodecRegistry - ): proto.Composite_Part { - if ('type' in content) { - const codec = codecs.codecFor(content.type) - if (!codec) { - throw new Error(`missing codec for part type ${content.type}`) - } - return { - part: codec.encode(content.content, codecs), - composite: undefined, - } - } - const parts = new Array() - for (const part of content.parts) { - parts.push(this.toProto(part, codecs)) - } - return { composite: { parts }, part: undefined } - } - - private fromProto( - content: proto.Composite_Part, - codecs: CodecRegistry - ): Composite { - if (content.part) { - if (!content.part.type) { - throw new Error('missing part content type') - } - const contentType = new ContentTypeId(content.part.type) - const codec = codecs.codecFor(contentType) - if (!codec) { - throw new Error(`missing codec for part type ${contentType}`) - } - return { - type: contentType, - content: codec.decode(content.part as EncodedContent, codecs), - } - } - if (!content.composite) { - throw new Error('invalid composite') - } - const parts = new Array() - for (const part of content.composite.parts) { - parts.push(this.fromProto(part, codecs)) - } - return { parts } - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - fallback(content: Composite): string | undefined { - return undefined - } - - shouldPush() { - return false - } -} 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..f9e1d0cee 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -43,15 +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 { ApiClient, QueryParams, 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..4903c1c1b 100644 --- a/packages/js-sdk/test/Client.test.ts +++ b/packages/js-sdk/test/Client.test.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 { message } from '@xmtp/proto' import type { PublishResponse } from '@xmtp/proto/ts/dist/types/message_api/v1/message_api.pb' import { Wallet } from 'ethers' @@ -7,8 +13,6 @@ import { mainnet } from 'viem/chains' 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' @@ -420,15 +424,51 @@ describe('ClientOptions', () => { }) it('allows you to use custom content types', async () => { + const ContentTypeCustom = new ContentTypeId({ + authorityId: 'xmtp.org', + typeId: 'text', + versionMajor: 1, + versionMinor: 0, + }) + class CustomCodec implements ContentCodec<{ custom: string }> { + get contentType(): ContentTypeId { + return ContentTypeCustom + } + + encode(content: { custom: string }): EncodedContent { + return { + type: ContentTypeText, + parameters: {}, + content: new TextEncoder().encode(JSON.stringify(content)), + } + } + + decode(content: EncodedContent): { custom: string } { + const decodedContent = new TextDecoder().decode(content.content) + const parsedContent = JSON.parse(decodedContent) as { custom: string } + return { + custom: parsedContent.custom, + } + } + + fallback() { + return undefined + } + + shouldPush() { + return false + } + } + const client = await Client.create(newWallet(), { + codecs: [new CustomCodec()], env: 'local', - codecs: [new CompositeCodec()], }) const other = await Client.create(newWallet(), { env: 'local' }) const convo = await client.conversations.newConversation(other.address) expect(convo).toBeTruthy() // This will have a type error if the codecs field isn't being respected - await convo.send({ parts: [{ type: ContentTypeText, content: 'foo' }] }) + await convo.send({ custom: 'test' }) }) }) 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 deleted file mode 100644 index 4790e524b..000000000 --- a/packages/js-sdk/test/codecs/Composite.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { CompositeCodec, ContentTypeComposite } from '@/codecs/Composite' -import { ContentTypeText } from '@/codecs/Text' -import { CodecRegistry } from '@test/helpers' - -describe('CompositeType', () => { - const codecs = new CodecRegistry() - const codec = new CompositeCodec() - codecs.registerCodec(codec) - it('simple composite', async () => { - const content = { - parts: [ - { type: ContentTypeText, content: 'hello' }, - { type: ContentTypeText, content: 'bye' }, - ], - } - const encoded = codec.encode(content, codecs) - expect(encoded.type.sameAs(ContentTypeComposite)).toBe(true) - const decoded = codec.decode(encoded, codecs) - expect(decoded).toEqual(content) - }) - it('nested composite', async () => { - const content = { - parts: [ - { type: ContentTypeText, content: 'one' }, - { - parts: [ - { type: ContentTypeText, content: 'two' }, - { - parts: [{ type: ContentTypeText, content: 'three' }], - }, - ], - }, - { - parts: [ - { type: ContentTypeText, content: 'four' }, - { - parts: [{ type: ContentTypeText, content: 'five' }], - }, - ], - }, - ], - } - const encoded = codec.encode(content, codecs) - expect(encoded.type.sameAs(ContentTypeComposite)).toBe(true) - const decoded = codec.decode(encoded, codecs) - expect(decoded).toEqual(content) - }) - - it('not quite composite decodes as single part composite', async () => { - const content = { type: ContentTypeText, content: 'one' } - const encoded = codec.encode(content, codecs) - expect(encoded.type.sameAs(ContentTypeComposite)).toBe(true) - const decoded = codec.decode(encoded, codecs) - expect(decoded).toEqual({ parts: [content] }) - }) - - it('definitely not a composite', () => { - const codec = codecs.codecFor(ContentTypeComposite) - expect(codec).toBeTruthy() - expect(() => codec!.encode('definitely not a composite', codecs)).toThrow() - }) -}) 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 b8fc044d3..ade7f07ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3002,6 +3002,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"