@defensestation/ysync-go 0.1.0-dev.3.3d87b03
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -0
- package/dist/client/connect-yjs-provider.d.ts +107 -0
- package/dist/client/connect-yjs-provider.d.ts.map +1 -0
- package/dist/client/connect-yjs-provider.js +333 -0
- package/dist/client/connect-yjs-provider.js.map +1 -0
- package/dist/client/id.d.ts +2 -0
- package/dist/client/id.d.ts.map +1 -0
- package/dist/client/id.js +21 -0
- package/dist/client/id.js.map +1 -0
- package/dist/client/provider-status.d.ts +8 -0
- package/dist/client/provider-status.d.ts.map +1 -0
- package/dist/client/provider-status.js +2 -0
- package/dist/client/provider-status.js.map +1 -0
- package/dist/client/websocket-yjs-provider.d.ts +51 -0
- package/dist/client/websocket-yjs-provider.d.ts.map +1 -0
- package/dist/client/websocket-yjs-provider.js +270 -0
- package/dist/client/websocket-yjs-provider.js.map +1 -0
- package/dist/gen/ysync/v1/sync_pb.d.ts +630 -0
- package/dist/gen/ysync/v1/sync_pb.d.ts.map +1 -0
- package/dist/gen/ysync/v1/sync_pb.js +133 -0
- package/dist/gen/ysync/v1/sync_pb.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/client/connect-yjs-provider.ts +358 -0
- package/src/client/id.ts +22 -0
- package/src/client/provider-status.ts +9 -0
- package/src/client/websocket-yjs-provider.ts +335 -0
- package/src/gen/ysync/v1/sync_pb.ts +732 -0
- package/src/index.ts +7 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// @generated by protoc-gen-es v2.12.1 with parameter "target=ts"
|
|
2
|
+
// @generated from file ysync/v1/sync.proto (package ysync.v1, syntax proto3)
|
|
3
|
+
/* eslint-disable */
|
|
4
|
+
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
|
5
|
+
/**
|
|
6
|
+
* Describes the file ysync/v1/sync.proto.
|
|
7
|
+
*/
|
|
8
|
+
export const file_ysync_v1_sync = /*@__PURE__*/ fileDesc("ChN5c3luYy92MS9zeW5jLnByb3RvEgh5c3luYy52MSLXAQoLU3luY1JlcXVlc3QSDgoGZG9jX2lkGAEgASgJEhEKCWNsaWVudF9pZBgCIAEoCRIiCgRqb2luGAogASgLMhIueXN5bmMudjEuU3luY0pvaW5IABIUCgp5anNfdXBkYXRlGAsgASgMSAASGgoQYXdhcmVuZXNzX3VwZGF0ZRgMIAEoDEgAEh4KBHBpbmcYDSABKAsyDi55c3luYy52MS5QaW5nSAASJAoFbGVhdmUYDiABKAsyEy55c3luYy52MS5TeW5jTGVhdmVIAEIJCgdwYXlsb2FkIjQKDFN5bmNSZXNwb25zZRIkCgVmcmFtZRgBIAEoCzIVLnlzeW5jLnYxLlNlcnZlckZyYW1lIiAKCFN5bmNKb2luEhQKDHN0YXRlX3ZlY3RvchgBIAEoDCILCglTeW5jTGVhdmUi5QEKC1NlcnZlckZyYW1lEg4KBmRvY19pZBgBIAEoCRIYChBzZW5kZXJfY2xpZW50X2lkGAIgASgJEhAKCHNlcXVlbmNlGAMgASgEEhUKC3N5bmNfdXBkYXRlGAogASgMSAASFAoKeWpzX3VwZGF0ZRgLIAEoDEgAEhoKEGF3YXJlbmVzc191cGRhdGUYDCABKAxIABIeCgRwb25nGA0gASgLMg4ueXN5bmMudjEuUG9uZ0gAEiYKBWVycm9yGA4gASgLMhUueXN5bmMudjEuU2VydmVyRXJyb3JIAEIJCgdwYXlsb2FkIk4KE0pvaW5Eb2N1bWVudFJlcXVlc3QSDgoGZG9jX2lkGAEgASgJEhEKCWNsaWVudF9pZBgCIAEoCRIUCgxzdGF0ZV92ZWN0b3IYAyABKAwidQoUSm9pbkRvY3VtZW50UmVzcG9uc2USEAoIc2VxdWVuY2UYASABKAQSFAoMc3luY191cGRhdGVzGAIgAygMEh0KFWFzc2lnbmVkX2Rpc3BsYXlfbmFtZRgDIAEoCRIWCg5hc3NpZ25lZF9jb2xvchgEIAEoCSJmChRQdXNoWWpzVXBkYXRlUmVxdWVzdBIOCgZkb2NfaWQYASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhIKCnlqc191cGRhdGUYAyABKAwSFwoPaWRlbXBvdGVuY3lfa2V5GAQgASgJIikKFVB1c2hZanNVcGRhdGVSZXNwb25zZRIQCghzZXF1ZW5jZRgBIAEoBCJZChpQdXNoQXdhcmVuZXNzVXBkYXRlUmVxdWVzdBIOCgZkb2NfaWQYASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhgKEGF3YXJlbmVzc191cGRhdGUYAyABKAwiLwobUHVzaEF3YXJlbmVzc1VwZGF0ZVJlc3BvbnNlEhAKCHNlcXVlbmNlGAEgASgEIlgKG1N0cmVhbURvY3VtZW50RnJhbWVzUmVxdWVzdBIOCgZkb2NfaWQYASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhYKDmFmdGVyX3NlcXVlbmNlGAMgASgEIkQKHFN0cmVhbURvY3VtZW50RnJhbWVzUmVzcG9uc2USJAoFZnJhbWUYASABKAsyFS55c3luYy52MS5TZXJ2ZXJGcmFtZSI5ChRMZWF2ZURvY3VtZW50UmVxdWVzdBIOCgZkb2NfaWQYASABKAkSEQoJY2xpZW50X2lkGAIgASgJIikKFUxlYXZlRG9jdW1lbnRSZXNwb25zZRIQCghzZXF1ZW5jZRgBIAEoBCJTChpMaXN0RG9jdW1lbnRVcGRhdGVzUmVxdWVzdBIOCgZkb2NfaWQYASABKAkSFgoOYWZ0ZXJfc2VxdWVuY2UYAiABKAQSDQoFbGltaXQYAyABKA0iTgobTGlzdERvY3VtZW50VXBkYXRlc1Jlc3BvbnNlEi8KB3VwZGF0ZXMYASADKAsyHi55c3luYy52MS5Eb2N1bWVudFVwZGF0ZVJlY29yZCKuAQoURG9jdW1lbnRVcGRhdGVSZWNvcmQSEAoIc2VxdWVuY2UYASABKAQSEQoJY2xpZW50X2lkGAIgASgJEhUKDWFjdG9yX3VzZXJfaWQYAyABKAkSGgoSYWN0b3JfZGlzcGxheV9uYW1lGAQgASgJEhUKDWNyZWF0ZWRfYXRfbXMYBSABKAMSEgoKeWpzX3VwZGF0ZRgGIAEoDBITCgtpc19zbmFwc2hvdBgHIAEoCCJiChZDb21wYWN0RG9jdW1lbnRSZXF1ZXN0Eg4KBmRvY19pZBgBIAEoCRIWCg5jb21wYWN0X3VwZGF0ZRgCIAEoDBIgChhjb21wYWN0X3Rocm91Z2hfc2VxdWVuY2UYAyABKAQiKwoXQ29tcGFjdERvY3VtZW50UmVzcG9uc2USEAoIc2VxdWVuY2UYASABKAQiFQoEUGluZxINCgVub25jZRgBIAEoCSIVCgRQb25nEg0KBW5vbmNlGAEgASgJIiwKC1NlcnZlckVycm9yEgwKBGNvZGUYASABKAkSDwoHbWVzc2FnZRgCIAEoCTLHBQoOWWpzU3luY1NlcnZpY2USOQoEU3luYxIVLnlzeW5jLnYxLlN5bmNSZXF1ZXN0GhYueXN5bmMudjEuU3luY1Jlc3BvbnNlKAEwARJNCgxKb2luRG9jdW1lbnQSHS55c3luYy52MS5Kb2luRG9jdW1lbnRSZXF1ZXN0Gh4ueXN5bmMudjEuSm9pbkRvY3VtZW50UmVzcG9uc2USUAoNUHVzaFlqc1VwZGF0ZRIeLnlzeW5jLnYxLlB1c2hZanNVcGRhdGVSZXF1ZXN0Gh8ueXN5bmMudjEuUHVzaFlqc1VwZGF0ZVJlc3BvbnNlEmIKE1B1c2hBd2FyZW5lc3NVcGRhdGUSJC55c3luYy52MS5QdXNoQXdhcmVuZXNzVXBkYXRlUmVxdWVzdBolLnlzeW5jLnYxLlB1c2hBd2FyZW5lc3NVcGRhdGVSZXNwb25zZRJnChRTdHJlYW1Eb2N1bWVudEZyYW1lcxIlLnlzeW5jLnYxLlN0cmVhbURvY3VtZW50RnJhbWVzUmVxdWVzdBomLnlzeW5jLnYxLlN0cmVhbURvY3VtZW50RnJhbWVzUmVzcG9uc2UwARJQCg1MZWF2ZURvY3VtZW50Eh4ueXN5bmMudjEuTGVhdmVEb2N1bWVudFJlcXVlc3QaHy55c3luYy52MS5MZWF2ZURvY3VtZW50UmVzcG9uc2USYgoTTGlzdERvY3VtZW50VXBkYXRlcxIkLnlzeW5jLnYxLkxpc3REb2N1bWVudFVwZGF0ZXNSZXF1ZXN0GiUueXN5bmMudjEuTGlzdERvY3VtZW50VXBkYXRlc1Jlc3BvbnNlElYKD0NvbXBhY3REb2N1bWVudBIgLnlzeW5jLnYxLkNvbXBhY3REb2N1bWVudFJlcXVlc3QaIS55c3luYy52MS5Db21wYWN0RG9jdW1lbnRSZXNwb25zZUI8WjpnaXRodWIuY29tL2RlZmVuc2VzdGF0aW9uL3lzeW5jLWdvL2dlbi9nby95c3luYy92MTt5c3luY3YxYgZwcm90bzM");
|
|
9
|
+
/**
|
|
10
|
+
* Describes the message ysync.v1.SyncRequest.
|
|
11
|
+
* Use `create(SyncRequestSchema)` to create a new message.
|
|
12
|
+
*/
|
|
13
|
+
export const SyncRequestSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 0);
|
|
14
|
+
/**
|
|
15
|
+
* Describes the message ysync.v1.SyncResponse.
|
|
16
|
+
* Use `create(SyncResponseSchema)` to create a new message.
|
|
17
|
+
*/
|
|
18
|
+
export const SyncResponseSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 1);
|
|
19
|
+
/**
|
|
20
|
+
* Describes the message ysync.v1.SyncJoin.
|
|
21
|
+
* Use `create(SyncJoinSchema)` to create a new message.
|
|
22
|
+
*/
|
|
23
|
+
export const SyncJoinSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 2);
|
|
24
|
+
/**
|
|
25
|
+
* Describes the message ysync.v1.SyncLeave.
|
|
26
|
+
* Use `create(SyncLeaveSchema)` to create a new message.
|
|
27
|
+
*/
|
|
28
|
+
export const SyncLeaveSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 3);
|
|
29
|
+
/**
|
|
30
|
+
* Describes the message ysync.v1.ServerFrame.
|
|
31
|
+
* Use `create(ServerFrameSchema)` to create a new message.
|
|
32
|
+
*/
|
|
33
|
+
export const ServerFrameSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 4);
|
|
34
|
+
/**
|
|
35
|
+
* Describes the message ysync.v1.JoinDocumentRequest.
|
|
36
|
+
* Use `create(JoinDocumentRequestSchema)` to create a new message.
|
|
37
|
+
*/
|
|
38
|
+
export const JoinDocumentRequestSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 5);
|
|
39
|
+
/**
|
|
40
|
+
* Describes the message ysync.v1.JoinDocumentResponse.
|
|
41
|
+
* Use `create(JoinDocumentResponseSchema)` to create a new message.
|
|
42
|
+
*/
|
|
43
|
+
export const JoinDocumentResponseSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 6);
|
|
44
|
+
/**
|
|
45
|
+
* Describes the message ysync.v1.PushYjsUpdateRequest.
|
|
46
|
+
* Use `create(PushYjsUpdateRequestSchema)` to create a new message.
|
|
47
|
+
*/
|
|
48
|
+
export const PushYjsUpdateRequestSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 7);
|
|
49
|
+
/**
|
|
50
|
+
* Describes the message ysync.v1.PushYjsUpdateResponse.
|
|
51
|
+
* Use `create(PushYjsUpdateResponseSchema)` to create a new message.
|
|
52
|
+
*/
|
|
53
|
+
export const PushYjsUpdateResponseSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 8);
|
|
54
|
+
/**
|
|
55
|
+
* Describes the message ysync.v1.PushAwarenessUpdateRequest.
|
|
56
|
+
* Use `create(PushAwarenessUpdateRequestSchema)` to create a new message.
|
|
57
|
+
*/
|
|
58
|
+
export const PushAwarenessUpdateRequestSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 9);
|
|
59
|
+
/**
|
|
60
|
+
* Describes the message ysync.v1.PushAwarenessUpdateResponse.
|
|
61
|
+
* Use `create(PushAwarenessUpdateResponseSchema)` to create a new message.
|
|
62
|
+
*/
|
|
63
|
+
export const PushAwarenessUpdateResponseSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 10);
|
|
64
|
+
/**
|
|
65
|
+
* Describes the message ysync.v1.StreamDocumentFramesRequest.
|
|
66
|
+
* Use `create(StreamDocumentFramesRequestSchema)` to create a new message.
|
|
67
|
+
*/
|
|
68
|
+
export const StreamDocumentFramesRequestSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 11);
|
|
69
|
+
/**
|
|
70
|
+
* Describes the message ysync.v1.StreamDocumentFramesResponse.
|
|
71
|
+
* Use `create(StreamDocumentFramesResponseSchema)` to create a new message.
|
|
72
|
+
*/
|
|
73
|
+
export const StreamDocumentFramesResponseSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 12);
|
|
74
|
+
/**
|
|
75
|
+
* Describes the message ysync.v1.LeaveDocumentRequest.
|
|
76
|
+
* Use `create(LeaveDocumentRequestSchema)` to create a new message.
|
|
77
|
+
*/
|
|
78
|
+
export const LeaveDocumentRequestSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 13);
|
|
79
|
+
/**
|
|
80
|
+
* Describes the message ysync.v1.LeaveDocumentResponse.
|
|
81
|
+
* Use `create(LeaveDocumentResponseSchema)` to create a new message.
|
|
82
|
+
*/
|
|
83
|
+
export const LeaveDocumentResponseSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 14);
|
|
84
|
+
/**
|
|
85
|
+
* Describes the message ysync.v1.ListDocumentUpdatesRequest.
|
|
86
|
+
* Use `create(ListDocumentUpdatesRequestSchema)` to create a new message.
|
|
87
|
+
*/
|
|
88
|
+
export const ListDocumentUpdatesRequestSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 15);
|
|
89
|
+
/**
|
|
90
|
+
* Describes the message ysync.v1.ListDocumentUpdatesResponse.
|
|
91
|
+
* Use `create(ListDocumentUpdatesResponseSchema)` to create a new message.
|
|
92
|
+
*/
|
|
93
|
+
export const ListDocumentUpdatesResponseSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 16);
|
|
94
|
+
/**
|
|
95
|
+
* Describes the message ysync.v1.DocumentUpdateRecord.
|
|
96
|
+
* Use `create(DocumentUpdateRecordSchema)` to create a new message.
|
|
97
|
+
*/
|
|
98
|
+
export const DocumentUpdateRecordSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 17);
|
|
99
|
+
/**
|
|
100
|
+
* Describes the message ysync.v1.CompactDocumentRequest.
|
|
101
|
+
* Use `create(CompactDocumentRequestSchema)` to create a new message.
|
|
102
|
+
*/
|
|
103
|
+
export const CompactDocumentRequestSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 18);
|
|
104
|
+
/**
|
|
105
|
+
* Describes the message ysync.v1.CompactDocumentResponse.
|
|
106
|
+
* Use `create(CompactDocumentResponseSchema)` to create a new message.
|
|
107
|
+
*/
|
|
108
|
+
export const CompactDocumentResponseSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 19);
|
|
109
|
+
/**
|
|
110
|
+
* Describes the message ysync.v1.Ping.
|
|
111
|
+
* Use `create(PingSchema)` to create a new message.
|
|
112
|
+
*/
|
|
113
|
+
export const PingSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 20);
|
|
114
|
+
/**
|
|
115
|
+
* Describes the message ysync.v1.Pong.
|
|
116
|
+
* Use `create(PongSchema)` to create a new message.
|
|
117
|
+
*/
|
|
118
|
+
export const PongSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 21);
|
|
119
|
+
/**
|
|
120
|
+
* Describes the message ysync.v1.ServerError.
|
|
121
|
+
* Use `create(ServerErrorSchema)` to create a new message.
|
|
122
|
+
*/
|
|
123
|
+
export const ServerErrorSchema = /*@__PURE__*/ messageDesc(file_ysync_v1_sync, 22);
|
|
124
|
+
/**
|
|
125
|
+
* YjsSyncService is the public Connect RPC contract for synchronizing
|
|
126
|
+
* Yjs documents. Yjs updates remain the canonical document state; projections,
|
|
127
|
+
* imports, exports, AI, search, and editor-specific formats belong outside this
|
|
128
|
+
* wire API.
|
|
129
|
+
*
|
|
130
|
+
* @generated from service ysync.v1.YjsSyncService
|
|
131
|
+
*/
|
|
132
|
+
export const YjsSyncService = /*@__PURE__*/ serviceDesc(file_ysync_v1_sync, 0);
|
|
133
|
+
//# sourceMappingURL=sync_pb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync_pb.js","sourceRoot":"","sources":["../../../../src/gen/ysync/v1/sync_pb.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ConnectYjsProvider } from "./client/connect-yjs-provider.js";
|
|
2
|
+
export type { ConnectYjsProviderOptions, YjsSyncRpcClient } from "./client/connect-yjs-provider.js";
|
|
3
|
+
export { WebSocketYjsProvider } from "./client/websocket-yjs-provider.js";
|
|
4
|
+
export type { WebSocketYjsProviderOptions } from "./client/websocket-yjs-provider.js";
|
|
5
|
+
export { makeClientId } from "./client/id.js";
|
|
6
|
+
export type { YjsProviderStatus, YjsProviderStatusListener, YjsProviderIdentity, YjsProviderIdentityListener } from "./client/provider-status.js";
|
|
7
|
+
export * from "./gen/ysync/v1/sync_pb.js";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AACtE,YAAY,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpG,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,YAAY,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACtF,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAClJ,cAAc,2BAA2B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ConnectYjsProvider } from "./client/connect-yjs-provider.js";
|
|
2
|
+
export { WebSocketYjsProvider } from "./client/websocket-yjs-provider.js";
|
|
3
|
+
export { makeClientId } from "./client/id.js";
|
|
4
|
+
export * from "./gen/ysync/v1/sync_pb.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAE1E,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,cAAc,2BAA2B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@defensestation/ysync-go",
|
|
3
|
+
"version": "0.1.0-dev.3.3d87b03",
|
|
4
|
+
"description": "Browser Yjs providers (Connect RPC and WebSocket) for the ysync-go collaboration backend",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/defensestation/ysync-go.git",
|
|
9
|
+
"directory": "npm"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/defensestation/ysync-go#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/defensestation/ysync-go/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"yjs",
|
|
17
|
+
"crdt",
|
|
18
|
+
"collaboration",
|
|
19
|
+
"realtime",
|
|
20
|
+
"connectrpc",
|
|
21
|
+
"websocket"
|
|
22
|
+
],
|
|
23
|
+
"private": false,
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./ysync/v1": {
|
|
34
|
+
"types": "./dist/gen/ysync/v1/sync_pb.d.ts",
|
|
35
|
+
"import": "./dist/gen/ysync/v1/sync_pb.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"src"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"generate": "cd .. && PATH=\"./npm/node_modules/.bin:$PATH\" buf generate --template npm/buf.gen.yaml",
|
|
44
|
+
"build": "npm run generate && tsc -p tsconfig.json",
|
|
45
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"prepack": "npm run build"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@bufbuild/protobuf": "^2.12.1",
|
|
51
|
+
"@connectrpc/connect": "^2.1.2",
|
|
52
|
+
"@connectrpc/connect-web": "^2.1.2",
|
|
53
|
+
"y-protocols": "^1.0.6",
|
|
54
|
+
"yjs": "^13.6.27"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@bufbuild/buf": "^1.57.2",
|
|
58
|
+
"@bufbuild/protoc-gen-es": "^2.12.1",
|
|
59
|
+
"@types/ws": "^8.18.1",
|
|
60
|
+
"typescript": "^5.9.2",
|
|
61
|
+
"vitest": "^3.2.4",
|
|
62
|
+
"ws": "^8.18.3"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { Code, ConnectError, createClient, type Interceptor } from "@connectrpc/connect";
|
|
2
|
+
import { createConnectTransport } from "@connectrpc/connect-web";
|
|
3
|
+
import { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate } from "y-protocols/awareness";
|
|
4
|
+
import * as Y from "yjs";
|
|
5
|
+
import { type ServerFrame, YjsSyncService } from "../gen/ysync/v1/sync_pb.js";
|
|
6
|
+
import { makeClientId } from "./id.js";
|
|
7
|
+
import type { YjsProviderIdentity, YjsProviderIdentityListener, YjsProviderStatus, YjsProviderStatusListener } from "./provider-status.js";
|
|
8
|
+
|
|
9
|
+
const REMOTE_ORIGIN = "connectrpc-remote";
|
|
10
|
+
const DEFAULT_RECONNECT_INITIAL_DELAY_MS = 500;
|
|
11
|
+
const DEFAULT_RECONNECT_MAX_DELAY_MS = 5000;
|
|
12
|
+
const DEFAULT_AWARENESS_THROTTLE_MS = 150;
|
|
13
|
+
const DEFAULT_UPDATE_FLUSH_DELAY_MS = 100;
|
|
14
|
+
|
|
15
|
+
function defaultBaseUrl(): string {
|
|
16
|
+
return typeof window !== "undefined" && window.location?.origin ? window.location.origin : "http://localhost:8080";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createDefaultClient(baseUrl: string, user?: { name: string; color?: string }) {
|
|
20
|
+
const interceptors: Interceptor[] = [];
|
|
21
|
+
if (user) {
|
|
22
|
+
// Actor identity for audit history; the server reads these headers on
|
|
23
|
+
// every RPC. Real deployments should derive identity from auth instead.
|
|
24
|
+
const asciiOnly = (value: string) => /^[\x20-\x7E]*$/.test(value);
|
|
25
|
+
interceptors.push((next) => (req) => {
|
|
26
|
+
if (asciiOnly(user.name)) req.header.set("x-user-name", user.name);
|
|
27
|
+
if (user.color && asciiOnly(user.color)) req.header.set("x-user-color", user.color);
|
|
28
|
+
return next(req);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return createClient(YjsSyncService, createConnectTransport({ baseUrl, useBinaryFormat: true, interceptors }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type YjsSyncRpcClient = ReturnType<typeof createDefaultClient>;
|
|
35
|
+
|
|
36
|
+
export interface ConnectYjsProviderOptions {
|
|
37
|
+
ydoc: Y.Doc;
|
|
38
|
+
docId: string;
|
|
39
|
+
baseUrl?: string;
|
|
40
|
+
rpcClient?: YjsSyncRpcClient;
|
|
41
|
+
clientId?: string;
|
|
42
|
+
user: { name: string; color?: string };
|
|
43
|
+
reconnectInitialDelayMs?: number;
|
|
44
|
+
reconnectMaxDelayMs?: number;
|
|
45
|
+
awarenessThrottleMs?: number;
|
|
46
|
+
// Micro-batch window for document updates: the first local update in a
|
|
47
|
+
// window is pushed immediately, updates arriving within the window are
|
|
48
|
+
// merged into one request. 0 pushes every update individually.
|
|
49
|
+
updateFlushDelayMs?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Browser-compatible Connect RPC provider. Browsers cannot open bidirectional
|
|
53
|
+
// Connect streams (fetch has no streaming request bodies), so this provider
|
|
54
|
+
// uses the unary/server-stream workflow from the proto contract:
|
|
55
|
+
// JoinDocument -> StreamDocumentFrames + PushYjsUpdate/PushAwarenessUpdate,
|
|
56
|
+
// then LeaveDocument on teardown. The bidirectional Sync RPC remains available
|
|
57
|
+
// for clients on transports that support it (e.g. connect-go over HTTP/2).
|
|
58
|
+
export class ConnectYjsProvider {
|
|
59
|
+
readonly awareness: Awareness;
|
|
60
|
+
readonly clientId: string;
|
|
61
|
+
|
|
62
|
+
private readonly client: YjsSyncRpcClient;
|
|
63
|
+
private readonly options: Required<Pick<ConnectYjsProviderOptions, "reconnectInitialDelayMs" | "reconnectMaxDelayMs" | "awarenessThrottleMs" | "updateFlushDelayMs">> & ConnectYjsProviderOptions;
|
|
64
|
+
private abortController?: AbortController;
|
|
65
|
+
private reconnectTimer?: ReturnType<typeof setTimeout>;
|
|
66
|
+
private awarenessSendTimer?: ReturnType<typeof setTimeout>;
|
|
67
|
+
private awarenessDirty = false;
|
|
68
|
+
private updateFlushTimer?: ReturnType<typeof setTimeout>;
|
|
69
|
+
private updatesDirty = false;
|
|
70
|
+
private destroyed = false;
|
|
71
|
+
private joined = false;
|
|
72
|
+
private reconnectDelayMs: number;
|
|
73
|
+
private pendingUpdates: Uint8Array[] = [];
|
|
74
|
+
// The current batch is frozen with its idempotency key so a retry after a
|
|
75
|
+
// failed push resends the exact same payload; updates queued in the
|
|
76
|
+
// meantime merge into the NEXT batch.
|
|
77
|
+
private inFlightBatch?: { key: string; update: Uint8Array };
|
|
78
|
+
private pushInFlight = false;
|
|
79
|
+
private updateCounter = 0;
|
|
80
|
+
private currentStatus: YjsProviderStatus = "connecting";
|
|
81
|
+
private readonly statusListeners = new Set<YjsProviderStatusListener>();
|
|
82
|
+
private currentIdentity?: YjsProviderIdentity;
|
|
83
|
+
private readonly identityListeners = new Set<YjsProviderIdentityListener>();
|
|
84
|
+
|
|
85
|
+
private readonly yjsUpdateHandler = (update: Uint8Array, origin: unknown) => {
|
|
86
|
+
if (origin === REMOTE_ORIGIN) return;
|
|
87
|
+
this.pendingUpdates.push(update);
|
|
88
|
+
this.scheduleUpdateFlush();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Only push changes to the LOCAL awareness state. Remote states arrive from
|
|
92
|
+
// the server already fanned out; re-broadcasting them would multiply every
|
|
93
|
+
// cursor move by the number of connected clients.
|
|
94
|
+
private readonly awarenessUpdateHandler = ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }) => {
|
|
95
|
+
const local = this.awareness.clientID;
|
|
96
|
+
if (!added.includes(local) && !updated.includes(local) && !removed.includes(local)) return;
|
|
97
|
+
this.scheduleAwarenessSend();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
constructor(options: ConnectYjsProviderOptions) {
|
|
101
|
+
this.options = {
|
|
102
|
+
...options,
|
|
103
|
+
reconnectInitialDelayMs: options.reconnectInitialDelayMs ?? DEFAULT_RECONNECT_INITIAL_DELAY_MS,
|
|
104
|
+
reconnectMaxDelayMs: options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS,
|
|
105
|
+
awarenessThrottleMs: options.awarenessThrottleMs ?? DEFAULT_AWARENESS_THROTTLE_MS,
|
|
106
|
+
updateFlushDelayMs: options.updateFlushDelayMs ?? DEFAULT_UPDATE_FLUSH_DELAY_MS,
|
|
107
|
+
};
|
|
108
|
+
this.clientId = options.clientId ?? makeClientId();
|
|
109
|
+
this.client = options.rpcClient ?? createDefaultClient(options.baseUrl ?? defaultBaseUrl(), options.user);
|
|
110
|
+
this.reconnectDelayMs = this.options.reconnectInitialDelayMs;
|
|
111
|
+
this.awareness = new Awareness(options.ydoc);
|
|
112
|
+
this.awareness.setLocalStateField("user", { name: options.user.name, color: options.user.color });
|
|
113
|
+
this.options.ydoc.on("update", this.yjsUpdateHandler);
|
|
114
|
+
// No extra heartbeat timer: y-protocols renews the local awareness state
|
|
115
|
+
// (bumping its clock) roughly every 15s while idle, which fires this
|
|
116
|
+
// handler. A fixed-interval resend of the same clock would be ignored by
|
|
117
|
+
// receivers anyway.
|
|
118
|
+
this.awareness.on("update", this.awarenessUpdateHandler);
|
|
119
|
+
this.connect();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get status(): YjsProviderStatus {
|
|
123
|
+
return this.currentStatus;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Server-assigned presence identity; undefined until the join completes.
|
|
127
|
+
get identity(): YjsProviderIdentity | undefined {
|
|
128
|
+
return this.currentIdentity;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Returns an unsubscribe function.
|
|
132
|
+
onIdentity(listener: YjsProviderIdentityListener): () => void {
|
|
133
|
+
this.identityListeners.add(listener);
|
|
134
|
+
return () => this.identityListeners.delete(listener);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private applyIdentity(name?: string, color?: string) {
|
|
138
|
+
if (!name && !color) return;
|
|
139
|
+
const identity: YjsProviderIdentity = {
|
|
140
|
+
name: name || this.options.user.name,
|
|
141
|
+
color: color ?? this.options.user.color,
|
|
142
|
+
};
|
|
143
|
+
this.currentIdentity = identity;
|
|
144
|
+
this.awareness.setLocalStateField("user", { name: identity.name, color: identity.color });
|
|
145
|
+
for (const listener of this.identityListeners) listener(identity);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Returns an unsubscribe function.
|
|
149
|
+
onStatus(listener: YjsProviderStatusListener): () => void {
|
|
150
|
+
this.statusListeners.add(listener);
|
|
151
|
+
return () => this.statusListeners.delete(listener);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private setStatus(status: YjsProviderStatus) {
|
|
155
|
+
if (this.currentStatus === status) return;
|
|
156
|
+
this.currentStatus = status;
|
|
157
|
+
for (const listener of this.statusListeners) listener(status);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
destroy() {
|
|
161
|
+
if (this.destroyed) return;
|
|
162
|
+
this.options.ydoc.off("update", this.yjsUpdateHandler);
|
|
163
|
+
this.awareness.off("update", this.awarenessUpdateHandler);
|
|
164
|
+
// Announce departure so peers drop the cursor immediately instead of
|
|
165
|
+
// after the 30s awareness timeout.
|
|
166
|
+
this.awareness.setLocalState(null);
|
|
167
|
+
if (this.joined) {
|
|
168
|
+
void this.client
|
|
169
|
+
.pushAwarenessUpdate({
|
|
170
|
+
docId: this.options.docId,
|
|
171
|
+
clientId: this.clientId,
|
|
172
|
+
awarenessUpdate: encodeAwarenessUpdate(this.awareness, [this.awareness.clientID]),
|
|
173
|
+
})
|
|
174
|
+
.catch(() => undefined);
|
|
175
|
+
}
|
|
176
|
+
this.destroyed = true;
|
|
177
|
+
this.setStatus("disconnected");
|
|
178
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
179
|
+
if (this.awarenessSendTimer) clearTimeout(this.awarenessSendTimer);
|
|
180
|
+
if (this.updateFlushTimer) clearTimeout(this.updateFlushTimer);
|
|
181
|
+
void this.client
|
|
182
|
+
.leaveDocument({ docId: this.options.docId, clientId: this.clientId })
|
|
183
|
+
.catch(() => undefined);
|
|
184
|
+
this.abortController?.abort();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private connect() {
|
|
188
|
+
if (this.destroyed) return;
|
|
189
|
+
this.abortController?.abort();
|
|
190
|
+
this.abortController = new AbortController();
|
|
191
|
+
this.setStatus("connecting");
|
|
192
|
+
void this.runSession(this.abortController.signal);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async runSession(signal: AbortSignal) {
|
|
196
|
+
try {
|
|
197
|
+
const join = await this.client.joinDocument(
|
|
198
|
+
{
|
|
199
|
+
docId: this.options.docId,
|
|
200
|
+
clientId: this.clientId,
|
|
201
|
+
stateVector: Y.encodeStateVector(this.options.ydoc),
|
|
202
|
+
},
|
|
203
|
+
{ signal },
|
|
204
|
+
);
|
|
205
|
+
for (const update of join.syncUpdates) {
|
|
206
|
+
if (update.length > 0) Y.applyUpdate(this.options.ydoc, update, REMOTE_ORIGIN);
|
|
207
|
+
}
|
|
208
|
+
this.joined = true;
|
|
209
|
+
this.applyIdentity(join.assignedDisplayName, join.assignedColor);
|
|
210
|
+
this.setStatus("connected");
|
|
211
|
+
this.reconnectDelayMs = this.options.reconnectInitialDelayMs;
|
|
212
|
+
this.sendLocalAwarenessNow();
|
|
213
|
+
void this.flushUpdates();
|
|
214
|
+
|
|
215
|
+
const frames = this.client.streamDocumentFrames(
|
|
216
|
+
{
|
|
217
|
+
docId: this.options.docId,
|
|
218
|
+
clientId: this.clientId,
|
|
219
|
+
afterSequence: join.sequence,
|
|
220
|
+
},
|
|
221
|
+
{ signal },
|
|
222
|
+
);
|
|
223
|
+
for await (const response of frames) {
|
|
224
|
+
if (response.frame) this.applyFrame(response.frame);
|
|
225
|
+
}
|
|
226
|
+
this.joined = false;
|
|
227
|
+
if (!this.destroyed && !signal.aborted) {
|
|
228
|
+
this.setStatus("disconnected");
|
|
229
|
+
this.scheduleReconnect();
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
this.joined = false;
|
|
233
|
+
if (!this.destroyed && !isAbortError(error)) {
|
|
234
|
+
this.setStatus("disconnected");
|
|
235
|
+
console.error("Yjs sync stream failed", error);
|
|
236
|
+
this.scheduleReconnect();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private scheduleReconnect() {
|
|
242
|
+
if (this.destroyed || this.reconnectTimer) return;
|
|
243
|
+
const delay = this.reconnectDelayMs;
|
|
244
|
+
this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, this.options.reconnectMaxDelayMs);
|
|
245
|
+
this.reconnectTimer = setTimeout(() => {
|
|
246
|
+
this.reconnectTimer = undefined;
|
|
247
|
+
this.connect();
|
|
248
|
+
}, delay);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Leading + trailing micro-batch, same shape as the awareness throttle.
|
|
252
|
+
private scheduleUpdateFlush() {
|
|
253
|
+
if (this.destroyed) return;
|
|
254
|
+
if (this.options.updateFlushDelayMs <= 0) {
|
|
255
|
+
void this.flushUpdates();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (this.updateFlushTimer) {
|
|
259
|
+
this.updatesDirty = true;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
void this.flushUpdates();
|
|
263
|
+
this.updateFlushTimer = setTimeout(() => {
|
|
264
|
+
this.updateFlushTimer = undefined;
|
|
265
|
+
if (this.updatesDirty) {
|
|
266
|
+
this.updatesDirty = false;
|
|
267
|
+
this.scheduleUpdateFlush();
|
|
268
|
+
}
|
|
269
|
+
}, this.options.updateFlushDelayMs);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private async flushUpdates() {
|
|
273
|
+
if (this.pushInFlight || !this.joined) return;
|
|
274
|
+
this.pushInFlight = true;
|
|
275
|
+
try {
|
|
276
|
+
while ((this.inFlightBatch || this.pendingUpdates.length > 0) && this.joined && !this.destroyed) {
|
|
277
|
+
if (!this.inFlightBatch) {
|
|
278
|
+
// Merge everything queued so far into one push. Under network
|
|
279
|
+
// latency this naturally batches keystrokes: while one push is in
|
|
280
|
+
// flight, new updates accumulate and go out as a single request.
|
|
281
|
+
const batch = this.pendingUpdates.splice(0);
|
|
282
|
+
this.inFlightBatch = {
|
|
283
|
+
key: `${this.clientId}-${this.updateCounter++}`,
|
|
284
|
+
update: batch.length === 1 ? batch[0] : Y.mergeUpdates(batch),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
await this.client.pushYjsUpdate({
|
|
289
|
+
docId: this.options.docId,
|
|
290
|
+
clientId: this.clientId,
|
|
291
|
+
yjsUpdate: this.inFlightBatch.update,
|
|
292
|
+
idempotencyKey: this.inFlightBatch.key,
|
|
293
|
+
});
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (!this.destroyed && !isAbortError(error)) {
|
|
296
|
+
console.error("Yjs update push failed", error);
|
|
297
|
+
this.scheduleReconnect();
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
this.inFlightBatch = undefined;
|
|
302
|
+
}
|
|
303
|
+
} finally {
|
|
304
|
+
this.pushInFlight = false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Leading + trailing throttle: the first change sends immediately, changes
|
|
309
|
+
// arriving inside the window (e.g. cursor movement while typing or
|
|
310
|
+
// selecting) collapse into one trailing send.
|
|
311
|
+
private scheduleAwarenessSend() {
|
|
312
|
+
if (this.destroyed) return;
|
|
313
|
+
if (this.awarenessSendTimer) {
|
|
314
|
+
this.awarenessDirty = true;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this.sendLocalAwarenessNow();
|
|
318
|
+
this.awarenessSendTimer = setTimeout(() => {
|
|
319
|
+
this.awarenessSendTimer = undefined;
|
|
320
|
+
if (this.awarenessDirty) {
|
|
321
|
+
this.awarenessDirty = false;
|
|
322
|
+
this.scheduleAwarenessSend();
|
|
323
|
+
}
|
|
324
|
+
}, this.options.awarenessThrottleMs);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private sendLocalAwarenessNow() {
|
|
328
|
+
if (this.destroyed || !this.joined) return;
|
|
329
|
+
void this.client
|
|
330
|
+
.pushAwarenessUpdate({
|
|
331
|
+
docId: this.options.docId,
|
|
332
|
+
clientId: this.clientId,
|
|
333
|
+
awarenessUpdate: encodeAwarenessUpdate(this.awareness, [this.awareness.clientID]),
|
|
334
|
+
})
|
|
335
|
+
.catch(() => undefined);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private applyFrame(frame: ServerFrame) {
|
|
339
|
+
switch (frame.payload.case) {
|
|
340
|
+
case "syncUpdate":
|
|
341
|
+
case "yjsUpdate":
|
|
342
|
+
Y.applyUpdate(this.options.ydoc, frame.payload.value, REMOTE_ORIGIN);
|
|
343
|
+
this.reconnectDelayMs = this.options.reconnectInitialDelayMs;
|
|
344
|
+
break;
|
|
345
|
+
case "awarenessUpdate":
|
|
346
|
+
applyAwarenessUpdate(this.awareness, frame.payload.value, REMOTE_ORIGIN);
|
|
347
|
+
break;
|
|
348
|
+
case "error":
|
|
349
|
+
console.error("Yjs sync server error", frame.payload.value);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function isAbortError(error: unknown): boolean {
|
|
356
|
+
if (error instanceof DOMException && error.name === "AbortError") return true;
|
|
357
|
+
return error instanceof ConnectError && error.code === Code.Canceled;
|
|
358
|
+
}
|
package/src/client/id.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function makeClientId() {
|
|
2
|
+
const cryptoApi = globalThis.crypto;
|
|
3
|
+
if (typeof cryptoApi?.randomUUID === "function") {
|
|
4
|
+
return cryptoApi.randomUUID();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (typeof cryptoApi?.getRandomValues === "function") {
|
|
8
|
+
const bytes = cryptoApi.getRandomValues(new Uint8Array(16));
|
|
9
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
10
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
11
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0"));
|
|
12
|
+
return [
|
|
13
|
+
hex.slice(0, 4).join(""),
|
|
14
|
+
hex.slice(4, 6).join(""),
|
|
15
|
+
hex.slice(6, 8).join(""),
|
|
16
|
+
hex.slice(8, 10).join(""),
|
|
17
|
+
hex.slice(10, 16).join(""),
|
|
18
|
+
].join("-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return `client-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type YjsProviderStatus = "connecting" | "connected" | "disconnected";
|
|
2
|
+
|
|
3
|
+
export type YjsProviderStatusListener = (status: YjsProviderStatus) => void;
|
|
4
|
+
|
|
5
|
+
// Presence identity assigned by the server at join time: sanitized display
|
|
6
|
+
// name plus a palette color that is distinct among the room's participants.
|
|
7
|
+
export type YjsProviderIdentity = { name: string; color?: string };
|
|
8
|
+
|
|
9
|
+
export type YjsProviderIdentityListener = (identity: YjsProviderIdentity) => void;
|