diff --git a/.eslintignore b/.eslintignore index 4d21e2a15..c02d9da25 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,3 +8,6 @@ packages/sdk/src/api/yorkie/v1/resources_grpc_web_pb.d.ts packages/sdk/src/api/yorkie/v1/resources_pb.d.ts packages/sdk/test/vitest.d.ts packages/sdk/lib + +# examples +examples/react-tldraw/src/tldraw.d.ts diff --git a/examples/react-tldraw/src/tldraw.d.ts b/examples/react-tldraw/src/tldraw.d.ts new file mode 100644 index 000000000..4def7c724 --- /dev/null +++ b/examples/react-tldraw/src/tldraw.d.ts @@ -0,0 +1,6 @@ +import { Indexable, Json } from '@yorkie-js-sdk/src/document/document'; +import { TDUser } from '@tldraw/tldraw'; + +declare module '@tldraw/tldraw' { + interface TDUser extends Indexable {} +} diff --git a/examples/vanilla-quill/src/main.ts b/examples/vanilla-quill/src/main.ts index 448ee1169..badd34070 100644 --- a/examples/vanilla-quill/src/main.ts +++ b/examples/vanilla-quill/src/main.ts @@ -29,7 +29,7 @@ function toDeltaOperation( ): DeltaOperation { const { embed, ...restAttributes } = textValue.attributes ?? {}; if (embed) { - return { insert: JSON.parse(embed), attributes: restAttributes }; + return { insert: embed, attributes: restAttributes }; } return { diff --git a/examples/vanilla-quill/src/type.ts b/examples/vanilla-quill/src/type.ts index 16dad89e4..e45bbfdc6 100644 --- a/examples/vanilla-quill/src/type.ts +++ b/examples/vanilla-quill/src/type.ts @@ -7,5 +7,5 @@ export type YorkieDoc = { export type YorkiePresence = { username: string; color: string; - selection: TextPosStructRange | undefined; + selection?: TextPosStructRange; }; diff --git a/packages/sdk/src/client/client.ts b/packages/sdk/src/client/client.ts index aa406ed40..bae869c59 100644 --- a/packages/sdk/src/client/client.ts +++ b/packages/sdk/src/client/client.ts @@ -43,6 +43,7 @@ import { OpSource } from '@yorkie-js-sdk/src/document/operation/operation'; import { createAuthInterceptor } from '@yorkie-js-sdk/src/client/auth_interceptor'; import { createMetricInterceptor } from '@yorkie-js-sdk/src/client/metric_interceptor'; import { validateSerializable } from '../util/validator'; +import { Json } from '@yorkie-js-sdk/src/document/document'; /** * `SyncMode` defines synchronization modes for the PushPullChanges API. @@ -607,7 +608,7 @@ export class Client { public broadcast( docKey: DocumentKey, topic: string, - payload: any, + payload: Json, ): Promise { if (!this.isActive()) { throw new YorkieError( diff --git a/packages/sdk/src/devtools/types.ts b/packages/sdk/src/devtools/types.ts index 87edebd57..7cf2efd89 100644 --- a/packages/sdk/src/devtools/types.ts +++ b/packages/sdk/src/devtools/types.ts @@ -17,20 +17,7 @@ import type { PrimitiveValue } from '@yorkie-js-sdk/src/document/crdt/primitive'; import type { CRDTTreePosStruct } from '@yorkie-js-sdk/src/document/crdt/tree'; import { CounterValue } from '@yorkie-js-sdk/src/document/crdt/counter'; - -/** - * `Json` represents a JSON value. - * - * TODO(hackerwins): We need to replace `Indexable` with `Json`. - */ -export type Json = - | string - | number - | boolean - // eslint-disable-next-line @typescript-eslint/ban-types - | null - | { [key: string]: Json } - | Array; +import { Json } from '@yorkie-js-sdk/src/document/document'; /** * `Client` represents a client value in devtools. diff --git a/packages/sdk/src/document/change/context.ts b/packages/sdk/src/document/change/context.ts index d097006d4..9f194824e 100644 --- a/packages/sdk/src/document/change/context.ts +++ b/packages/sdk/src/document/change/context.ts @@ -159,7 +159,7 @@ export class ChangeContext

{ const reversePresence: Partial

= {}; for (const key of this.reversePresenceKeys) { - reversePresence[key as keyof P] = this.previousPresence[key]; + reversePresence[key as keyof P] = this.previousPresence[key as keyof P]; } return reversePresence; } diff --git a/packages/sdk/src/document/document.ts b/packages/sdk/src/document/document.ts index c733829f9..311f91d3c 100644 --- a/packages/sdk/src/document/document.ts +++ b/packages/sdk/src/document/document.ts @@ -385,7 +385,7 @@ export interface PresenceChangedEvent

export interface BroadcastEvent extends BaseDocEvent { type: DocEventType.Broadcast; - value: { clientID: ActorID; topic: string; payload: any }; + value: { clientID: ActorID; topic: string; payload: Json }; error?: ErrorFn; } @@ -421,13 +421,25 @@ export type DocEventCallback

= DocEventCallbackMap

[DocEventTopic]; /** - * Indexable key, value + * `Json` represents the JSON data type. It is used to represent the data + * structure of the document. + */ +export type Json = JsonPrimitive | JsonArray | JsonObject; + +// eslint-disable-next-line @typescript-eslint/ban-types +type JsonPrimitive = string | number | boolean | null; +type JsonArray = Array; +type JsonObject = { [key: string]: Json | undefined }; + +/** + * `Indexable` represents the type of the indexable object. It is used to + * represent the presence information of the client. * @public */ -export type Indexable = Record; +export type Indexable = Record; /** - * Document key type + * `DocumentKey` represents the key of the document. * @public */ export type DocumentKey = string; @@ -2058,7 +2070,7 @@ export class Document { /** * `broadcast` the payload to the given topic. */ - public broadcast(topic: string, payload: any, error?: ErrorFn) { + public broadcast(topic: string, payload: Json, error?: ErrorFn) { const broadcastEvent: LocalBroadcastEvent = { type: DocEventType.LocalBroadcast, value: { topic, payload }, diff --git a/packages/sdk/src/document/json/tree.ts b/packages/sdk/src/document/json/tree.ts index b99161875..c84aacc4d 100644 --- a/packages/sdk/src/document/json/tree.ts +++ b/packages/sdk/src/document/json/tree.ts @@ -82,14 +82,14 @@ function buildDescendants( parent.append(textNode); } else { const { children = [] } = treeNode as ElementNode; - let { attributes } = treeNode as ElementNode; + const { attributes } = treeNode as ElementNode; let attrs; if (typeof attributes === 'object' && !isEmpty(attributes)) { - attributes = stringifyObjectValues(attributes); + const stringifiedAttributes = stringifyObjectValues(attributes); attrs = new RHT(); - for (const [key, value] of Object.entries(attributes)) { + for (const [key, value] of Object.entries(stringifiedAttributes)) { attrs.set(key, value, ticket); } } @@ -121,14 +121,14 @@ function createCRDTTreeNode(context: ChangeContext, content: TreeNode) { root = CRDTTreeNode.create(CRDTTreeNodeID.of(ticket, 0), type, value); } else if (content) { const { children = [] } = content as ElementNode; - let { attributes } = content as ElementNode; + const { attributes } = content as ElementNode; let attrs; if (typeof attributes === 'object' && !isEmpty(attributes)) { - attributes = stringifyObjectValues(attributes); + const stringifiedAttributes = stringifyObjectValues(attributes); attrs = new RHT(); - for (const [key, value] of Object.entries(attributes)) { + for (const [key, value] of Object.entries(stringifiedAttributes)) { attrs.set(key, value, ticket); } } diff --git a/packages/sdk/src/util/object.ts b/packages/sdk/src/util/object.ts index f26381683..3cb7286f6 100644 --- a/packages/sdk/src/util/object.ts +++ b/packages/sdk/src/util/object.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { Indexable } from '@yorkie-js-sdk/src/document/document'; + /** * `deepcopy` returns a deep copy of the given object. */ @@ -40,7 +42,7 @@ export const isEmpty = (object: object) => { /** * `stringifyObjectValues` makes values of attributes to JSON parsable string. */ -export const stringifyObjectValues = ( +export const stringifyObjectValues = ( attributes: A, ): Record => { const attrs: Record = {}; @@ -53,7 +55,7 @@ export const stringifyObjectValues = ( /** `parseObjectValues` returns the JSON parsable string values to the origin states. */ -export const parseObjectValues = ( +export const parseObjectValues = ( attrs: Record, ): A => { const attributes: Record = {}; diff --git a/packages/sdk/src/util/validator.ts b/packages/sdk/src/util/validator.ts index 33625d837..0d57912f3 100644 --- a/packages/sdk/src/util/validator.ts +++ b/packages/sdk/src/util/validator.ts @@ -17,7 +17,7 @@ /** * `validateSerializable` returns whether the given value is serializable or not. */ -export const validateSerializable = (value: any): boolean => { +export const validateSerializable = (value: unknown): boolean => { try { const serialized = JSON.stringify(value); diff --git a/packages/sdk/test/integration/client_test.ts b/packages/sdk/test/integration/client_test.ts index 004e0b506..2d485e386 100644 --- a/packages/sdk/test/integration/client_test.ts +++ b/packages/sdk/test/integration/client_test.ts @@ -900,6 +900,8 @@ describe.sequential('Client', function () { eventCollector.add(error.message); }; + // @ts-ignore + // Disable type checking for testing purposes doc.broadcast(broadcastTopic, payload, errorHandler); await eventCollector.waitAndVerifyNthEvent(1, broadcastErrMessage); diff --git a/packages/sdk/test/integration/integration_helper.ts b/packages/sdk/test/integration/integration_helper.ts index f0189e390..02f0a8ae4 100644 --- a/packages/sdk/test/integration/integration_helper.ts +++ b/packages/sdk/test/integration/integration_helper.ts @@ -13,12 +13,15 @@ export function toDocKey(title: string): string { .replace(/[^a-z0-9-]/g, '-'); } -export async function withTwoClientsAndDocuments( +export async function withTwoClientsAndDocuments< + T, + P extends Indexable = Indexable, +>( callback: ( c1: Client, - d1: Document, + d1: Document, c2: Client, - d2: Document, + d2: Document, ) => Promise, title: string, syncMode: SyncMode = SyncMode.Manual, @@ -29,8 +32,8 @@ export async function withTwoClientsAndDocuments( await client2.activate(); const docKey = `${toDocKey(title)}-${new Date().getTime()}`; - const doc1 = new yorkie.Document(docKey); - const doc2 = new yorkie.Document(docKey); + const doc1 = new yorkie.Document(docKey); + const doc2 = new yorkie.Document(docKey); await client1.attach(doc1, { syncMode }); await client2.attach(doc2, { syncMode });