From 2018fd20289dff52f1cf0ecc07fa5ac254d15e65 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Fri, 14 Jun 2024 10:43:55 -0500 Subject: [PATCH 1/5] 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 | 2 + 19 files changed, 26 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 c0faa9f1..33226675 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 1f15a606..907e09b9 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 e5179bbd..e92390d2 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 8ffb63e4..00000000 --- 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 e5a115e6..2bbe9f5d 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 af1ecb46..00000000 --- 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 340c3f54..a6bc7655 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 4e241590..45e889a7 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 a22d3019..8f7dd9af 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 21c38b40..01fd6555 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 e4da5191..00352be0 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 16fa87c0..e4d52dc7 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 99e999d7..b242eb3c 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 d3b17168..00000000 --- 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 4790e524..6d8c6357 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 daca2fc4..00000000 --- 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 41ded24b..db65a374 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 2f22ed1e..e8254b21 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 b8fc044d..ade7f07b 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" From 540af3117d76e3ff4f4dd0f369baefe4a31fa30c Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Fri, 14 Jun 2024 10:53:50 -0500 Subject: [PATCH 2/5] Update README --- packages/js-sdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js-sdk/README.md b/packages/js-sdk/README.md index cd2e4144..25003207 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. From b5d9ff6c0a4a3f9b5ff5016a2eedccfb46f64f6b Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Fri, 14 Jun 2024 11:06:25 -0500 Subject: [PATCH 3/5] Fix rollup config --- packages/js-sdk/rollup.config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/js-sdk/rollup.config.js b/packages/js-sdk/rollup.config.js index 6ac3ae4f..3415d65d 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', From 4f86be5c6320b5b971bbe1fae9342df0322ca5b8 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Fri, 14 Jun 2024 12:21:58 -0500 Subject: [PATCH 4/5] Remove CompositeContentType --- packages/js-sdk/README.md | 7 -- packages/js-sdk/src/codecs/Composite.ts | 118 ------------------ packages/js-sdk/src/index.ts | 2 - packages/js-sdk/test/Client.test.ts | 46 ++++++- packages/js-sdk/test/codecs/Composite.test.ts | 62 --------- 5 files changed, 43 insertions(+), 192 deletions(-) delete mode 100644 packages/js-sdk/src/codecs/Composite.ts delete mode 100644 packages/js-sdk/test/codecs/Composite.test.ts diff --git a/packages/js-sdk/README.md b/packages/js-sdk/README.md index 25003207..99891b08 100644 --- a/packages/js-sdk/README.md +++ b/packages/js-sdk/README.md @@ -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/src/codecs/Composite.ts b/packages/js-sdk/src/codecs/Composite.ts deleted file mode 100644 index 2bbe9f5d..00000000 --- a/packages/js-sdk/src/codecs/Composite.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - ContentTypeId, - type CodecRegistry, - type ContentCodec, - type EncodedContent, -} from '@xmtp/content-type-primitives' -import { composite as proto } from '@xmtp/proto' - -// 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/index.ts b/packages/js-sdk/src/index.ts index 45e889a7..f9e1d0ce 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -43,8 +43,6 @@ export { export type { Conversation } from '@/conversations/Conversation' export { ConversationV1, ConversationV2 } from '@/conversations/Conversation' export { default as Conversations } from '@/conversations/Conversations' -export type { Composite } from './codecs/Composite' -export { CompositeCodec, ContentTypeComposite } from './codecs/Composite' export type { ApiClient, QueryParams, diff --git a/packages/js-sdk/test/Client.test.ts b/packages/js-sdk/test/Client.test.ts index 01fd6555..4903c1c1 100644 --- a/packages/js-sdk/test/Client.test.ts +++ b/packages/js-sdk/test/Client.test.ts @@ -1,3 +1,8 @@ +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' @@ -8,7 +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 { 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/codecs/Composite.test.ts b/packages/js-sdk/test/codecs/Composite.test.ts deleted file mode 100644 index 6d8c6357..00000000 --- a/packages/js-sdk/test/codecs/Composite.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ContentTypeText } from '@xmtp/content-type-text' -import { CompositeCodec, ContentTypeComposite } from '@/codecs/Composite' -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() - }) -}) From 96193221ae9f47f0a869cb3c93e1067ad0e80b0d Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Fri, 14 Jun 2024 10:37:27 -0700 Subject: [PATCH 5/5] Create twenty-eggs-protect.md --- .changeset/twenty-eggs-protect.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .changeset/twenty-eggs-protect.md diff --git a/.changeset/twenty-eggs-protect.md b/.changeset/twenty-eggs-protect.md new file mode 100644 index 00000000..1cf6499d --- /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`.