@liveblocks/server 1.1.1-pnpmtest1 → 1.2.1

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/dist/index.js CHANGED
@@ -12,16 +12,139 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
12
12
  // src/decoders/ClientMsg.ts
13
13
  import { ClientMsgCode } from "@liveblocks/core";
14
14
  import {
15
- array,
15
+ array as array2,
16
16
  boolean,
17
17
  constant as constant2,
18
+ jsonObject,
19
+ nonEmptyString,
18
20
  number,
19
21
  object as object2,
20
- optional as optional2,
21
- string as string2,
22
+ optional as optional3,
23
+ string as string3,
22
24
  taggedUnion as taggedUnion2
23
25
  } from "decoders";
24
26
 
27
+ // src/protocol/feedMessages.ts
28
+ var FeedMsgCode = {
29
+ // Server → client (50x)
30
+ FEEDS_LIST: 500,
31
+ FEEDS_ADDED: 501,
32
+ FEEDS_UPDATED: 502,
33
+ FEED_DELETED: 503,
34
+ FEED_MESSAGES_LIST: 504,
35
+ FEED_MESSAGES_ADDED: 505,
36
+ FEED_MESSAGES_UPDATED: 506,
37
+ FEED_MESSAGES_DELETED: 507,
38
+ FEED_REQUEST_FAILED: 508,
39
+ // Client → server (51x)
40
+ FETCH_FEEDS: 510,
41
+ FETCH_FEED_MESSAGES: 511,
42
+ ADD_FEED: 512,
43
+ UPDATE_FEED: 513,
44
+ DELETE_FEED: 514,
45
+ ADD_FEED_MESSAGE: 515,
46
+ UPDATE_FEED_MESSAGE: 516,
47
+ DELETE_FEED_MESSAGE: 517
48
+ };
49
+ var FeedRequestErrorCode = {
50
+ INTERNAL: "INTERNAL",
51
+ FEED_ALREADY_EXISTS: "FEED_ALREADY_EXISTS",
52
+ FEED_NOT_FOUND: "FEED_NOT_FOUND",
53
+ FEED_MESSAGE_NOT_FOUND: "FEED_MESSAGE_NOT_FOUND"
54
+ };
55
+
56
+ // src/protocol/feedErrors.ts
57
+ function feedRequestFailed(requestId, code, reason) {
58
+ return {
59
+ type: FeedMsgCode.FEED_REQUEST_FAILED,
60
+ requestId: requestId ?? "",
61
+ code,
62
+ reason
63
+ };
64
+ }
65
+ function mapFeedError(err) {
66
+ if (!(err instanceof Error)) {
67
+ return { code: FeedRequestErrorCode.INTERNAL };
68
+ }
69
+ const m = err.message;
70
+ if (m.includes("already exists")) {
71
+ return { code: FeedRequestErrorCode.FEED_ALREADY_EXISTS, reason: m };
72
+ }
73
+ if (m.includes("Feed message") && m.includes("not found")) {
74
+ return { code: FeedRequestErrorCode.FEED_MESSAGE_NOT_FOUND, reason: m };
75
+ }
76
+ if (m.includes("not found")) {
77
+ return { code: FeedRequestErrorCode.FEED_NOT_FOUND, reason: m };
78
+ }
79
+ return { code: FeedRequestErrorCode.INTERNAL, reason: m };
80
+ }
81
+ function feedFailureServerMsg(requestId, err) {
82
+ const mapped = mapFeedError(err);
83
+ return feedRequestFailed(requestId, mapped.code, mapped.reason);
84
+ }
85
+
86
+ // src/protocol/ProtocolVersion.ts
87
+ import { enum_ } from "decoders";
88
+ var ProtocolVersion = /* @__PURE__ */ ((ProtocolVersion2) => {
89
+ ProtocolVersion2[ProtocolVersion2["V7"] = 7] = "V7";
90
+ ProtocolVersion2[ProtocolVersion2["V8"] = 8] = "V8";
91
+ return ProtocolVersion2;
92
+ })(ProtocolVersion || {});
93
+ var protocolVersionDecoder = enum_(ProtocolVersion).describe(
94
+ "Unsupported protocol version"
95
+ );
96
+
97
+ // src/decoders/feedMetadata.ts
98
+ import {
99
+ array,
100
+ either,
101
+ identifier,
102
+ nullable,
103
+ optional,
104
+ poja,
105
+ record,
106
+ select,
107
+ sized,
108
+ string
109
+ } from "decoders";
110
+ var MAX_METADATA_COUNT = 50;
111
+ var MAX_METADATA_VALUE_LIST_LENGTH = 50;
112
+ var feedMetadataIdDecoder = sized(identifier, { min: 1, max: 40 });
113
+ var metadataStringValue = sized(string, { max: 256 });
114
+ function createRoomMetadataValueDecoder() {
115
+ return select(
116
+ either(string, poja).describe("Must be string or string[]"),
117
+ (x) => typeof x === "string" ? metadataStringValue : array(metadataStringValue).refine(
118
+ (value) => value.length <= MAX_METADATA_VALUE_LIST_LENGTH,
119
+ `Must be at most ${MAX_METADATA_VALUE_LIST_LENGTH} items`
120
+ )
121
+ );
122
+ }
123
+ var roomMetadataValueDecoder = createRoomMetadataValueDecoder();
124
+ var feedMetadataNullableValueDecoder = nullable(roomMetadataValueDecoder);
125
+ var feedMetadataRecordForCreate = record(
126
+ feedMetadataIdDecoder,
127
+ roomMetadataValueDecoder
128
+ ).refine(
129
+ (value) => Object.keys(value).length <= MAX_METADATA_COUNT,
130
+ `Must be at most ${MAX_METADATA_COUNT} items`
131
+ );
132
+ var optionalFeedMetadataDecoder = optional(feedMetadataRecordForCreate);
133
+ var feedMetadataUpdateDecoder = record(
134
+ feedMetadataIdDecoder,
135
+ feedMetadataNullableValueDecoder
136
+ );
137
+ var feedMetadataRecordForFilter = record(
138
+ feedMetadataIdDecoder,
139
+ feedMetadataNullableValueDecoder
140
+ ).refine(
141
+ (value) => Object.keys(value).length <= MAX_METADATA_COUNT,
142
+ `Must be at most ${MAX_METADATA_COUNT} items`
143
+ );
144
+ var fetchFeedsMetadataFilterDecoder = optional(
145
+ feedMetadataRecordForFilter
146
+ );
147
+
25
148
  // src/decoders/jsonYolo.ts
26
149
  import { unknown } from "decoders";
27
150
  var jsonYolo = unknown;
@@ -32,67 +155,67 @@ var jsonObjectYolo = jsonYolo.refine(
32
155
 
33
156
  // src/decoders/Op.ts
34
157
  import { OpCode } from "@liveblocks/core";
35
- import { constant, object, optional, string, taggedUnion } from "decoders";
158
+ import { constant, object, optional as optional2, string as string2, taggedUnion } from "decoders";
36
159
  var updateObjectOp = object({
37
160
  type: constant(OpCode.UPDATE_OBJECT),
38
- opId: string,
39
- id: string,
161
+ opId: string2,
162
+ id: string2,
40
163
  data: jsonObjectYolo
41
164
  });
42
165
  var createObjectOp = object({
43
166
  type: constant(OpCode.CREATE_OBJECT),
44
- opId: string,
45
- id: string,
46
- parentId: string,
47
- parentKey: string,
167
+ opId: string2,
168
+ id: string2,
169
+ parentId: string2,
170
+ parentKey: string2,
48
171
  data: jsonObjectYolo,
49
- intent: optional(constant("set")),
50
- deletedId: optional(string)
172
+ intent: optional2(constant("set")),
173
+ deletedId: optional2(string2)
51
174
  });
52
175
  var createListOp = object({
53
176
  type: constant(OpCode.CREATE_LIST),
54
- opId: string,
55
- id: string,
56
- parentId: string,
57
- parentKey: string,
58
- intent: optional(constant("set")),
59
- deletedId: optional(string)
177
+ opId: string2,
178
+ id: string2,
179
+ parentId: string2,
180
+ parentKey: string2,
181
+ intent: optional2(constant("set")),
182
+ deletedId: optional2(string2)
60
183
  });
61
184
  var createMapOp = object({
62
185
  type: constant(OpCode.CREATE_MAP),
63
- opId: string,
64
- id: string,
65
- parentId: string,
66
- parentKey: string,
67
- intent: optional(constant("set")),
68
- deletedId: optional(string)
186
+ opId: string2,
187
+ id: string2,
188
+ parentId: string2,
189
+ parentKey: string2,
190
+ intent: optional2(constant("set")),
191
+ deletedId: optional2(string2)
69
192
  });
70
193
  var createRegisterOp = object({
71
194
  type: constant(OpCode.CREATE_REGISTER),
72
- opId: string,
73
- id: string,
74
- parentId: string,
75
- parentKey: string,
195
+ opId: string2,
196
+ id: string2,
197
+ parentId: string2,
198
+ parentKey: string2,
76
199
  data: jsonYolo,
77
- intent: optional(constant("set")),
78
- deletedId: optional(string)
200
+ intent: optional2(constant("set")),
201
+ deletedId: optional2(string2)
79
202
  });
80
203
  var deleteCrdtOp = object({
81
204
  type: constant(OpCode.DELETE_CRDT),
82
- opId: string,
83
- id: string
205
+ opId: string2,
206
+ id: string2
84
207
  });
85
208
  var setParentKeyOp = object({
86
209
  type: constant(OpCode.SET_PARENT_KEY),
87
- opId: string,
88
- id: string,
89
- parentKey: string
210
+ opId: string2,
211
+ id: string2,
212
+ parentKey: string2
90
213
  });
91
214
  var deleteObjectKeyOp = object({
92
215
  type: constant(OpCode.DELETE_OBJECT_KEY),
93
- opId: string,
94
- id: string,
95
- key: string
216
+ opId: string2,
217
+ id: string2,
218
+ key: string2
96
219
  });
97
220
  var op = taggedUnion("type", {
98
221
  [OpCode.UPDATE_OBJECT]: updateObjectOp,
@@ -114,7 +237,7 @@ var ROOT_YDOC_ID = "root";
114
237
  var updatePresenceClientMsg = object2({
115
238
  type: constant2(ClientMsgCode.UPDATE_PRESENCE),
116
239
  data: jsonObjectYolo,
117
- targetActor: optional2(number)
240
+ targetActor: optional3(number)
118
241
  });
119
242
  var broadcastEventClientMsg = object2({
120
243
  type: constant2(ClientMsgCode.BROADCAST_EVENT),
@@ -125,21 +248,77 @@ var fetchStorageClientMsg = object2({
125
248
  });
126
249
  var updateStorageClientMsg = object2({
127
250
  type: constant2(ClientMsgCode.UPDATE_STORAGE),
128
- ops: array(op)
251
+ ops: array2(op)
129
252
  });
130
253
  var fetchYDocClientMsg = object2({
131
254
  type: constant2(ClientMsgCode.FETCH_YDOC),
132
- vector: string2.refineType(),
133
- guid: optional2(guidDecoder),
255
+ vector: string3.refineType(),
256
+ guid: optional3(guidDecoder),
134
257
  // Don't specify to update the root doc
135
- v2: optional2(boolean)
258
+ v2: optional3(boolean)
136
259
  });
137
260
  var updateYDocClientMsg = object2({
138
261
  type: constant2(ClientMsgCode.UPDATE_YDOC),
139
- update: string2.refineType(),
140
- guid: optional2(guidDecoder),
262
+ update: string3.refineType(),
263
+ guid: optional3(guidDecoder),
141
264
  // Don't specify to update the root doc
142
- v2: optional2(boolean)
265
+ v2: optional3(boolean)
266
+ });
267
+ var fetchFeedsClientMsg = object2({
268
+ type: constant2(FeedMsgCode.FETCH_FEEDS),
269
+ requestId: string3,
270
+ cursor: optional3(string3),
271
+ since: optional3(number),
272
+ limit: optional3(number),
273
+ metadata: fetchFeedsMetadataFilterDecoder
274
+ });
275
+ var fetchFeedMessagesClientMsg = object2({
276
+ type: constant2(FeedMsgCode.FETCH_FEED_MESSAGES),
277
+ requestId: string3,
278
+ feedId: nonEmptyString,
279
+ cursor: optional3(string3),
280
+ since: optional3(number),
281
+ limit: optional3(number)
282
+ });
283
+ var addFeedClientMsg = object2({
284
+ type: constant2(FeedMsgCode.ADD_FEED),
285
+ feedId: string3,
286
+ metadata: optionalFeedMetadataDecoder,
287
+ timestamp: optional3(number),
288
+ requestId: optional3(string3)
289
+ });
290
+ var updateFeedClientMsg = object2({
291
+ type: constant2(FeedMsgCode.UPDATE_FEED),
292
+ feedId: string3,
293
+ metadata: feedMetadataUpdateDecoder,
294
+ requestId: optional3(string3)
295
+ });
296
+ var deleteFeedClientMsg = object2({
297
+ type: constant2(FeedMsgCode.DELETE_FEED),
298
+ feedId: string3,
299
+ requestId: optional3(string3)
300
+ });
301
+ var addFeedMessageClientMsg = object2({
302
+ type: constant2(FeedMsgCode.ADD_FEED_MESSAGE),
303
+ feedId: string3,
304
+ data: jsonObject,
305
+ id: optional3(string3),
306
+ timestamp: optional3(number),
307
+ requestId: optional3(string3)
308
+ });
309
+ var updateFeedMessageClientMsg = object2({
310
+ type: constant2(FeedMsgCode.UPDATE_FEED_MESSAGE),
311
+ feedId: string3,
312
+ messageId: string3,
313
+ data: jsonObject,
314
+ timestamp: optional3(number),
315
+ requestId: optional3(string3)
316
+ });
317
+ var deleteFeedMessageClientMsg = object2({
318
+ type: constant2(FeedMsgCode.DELETE_FEED_MESSAGE),
319
+ feedId: string3,
320
+ messageId: string3,
321
+ requestId: optional3(string3)
143
322
  });
144
323
  var clientMsgDecoder = taggedUnion2("type", {
145
324
  [ClientMsgCode.UPDATE_PRESENCE]: updatePresenceClientMsg,
@@ -147,7 +326,15 @@ var clientMsgDecoder = taggedUnion2("type", {
147
326
  [ClientMsgCode.FETCH_STORAGE]: fetchStorageClientMsg,
148
327
  [ClientMsgCode.UPDATE_STORAGE]: updateStorageClientMsg,
149
328
  [ClientMsgCode.FETCH_YDOC]: fetchYDocClientMsg,
150
- [ClientMsgCode.UPDATE_YDOC]: updateYDocClientMsg
329
+ [ClientMsgCode.UPDATE_YDOC]: updateYDocClientMsg,
330
+ [FeedMsgCode.FETCH_FEEDS]: fetchFeedsClientMsg,
331
+ [FeedMsgCode.FETCH_FEED_MESSAGES]: fetchFeedMessagesClientMsg,
332
+ [FeedMsgCode.ADD_FEED]: addFeedClientMsg,
333
+ [FeedMsgCode.UPDATE_FEED]: updateFeedClientMsg,
334
+ [FeedMsgCode.DELETE_FEED]: deleteFeedClientMsg,
335
+ [FeedMsgCode.ADD_FEED_MESSAGE]: addFeedMessageClientMsg,
336
+ [FeedMsgCode.UPDATE_FEED_MESSAGE]: updateFeedMessageClientMsg,
337
+ [FeedMsgCode.DELETE_FEED_MESSAGE]: deleteFeedMessageClientMsg
151
338
  }).describe("Must be a valid client message");
152
339
  var transientClientMsgDecoder = taggedUnion2("type", {
153
340
  // [ClientMsgCode.UPDATE_PRESENCE]: updatePresenceClientMsg,
@@ -628,17 +815,6 @@ function makeMetadataDB(driver) {
628
815
  };
629
816
  }
630
817
 
631
- // src/protocol/ProtocolVersion.ts
632
- import { enum_ } from "decoders";
633
- var ProtocolVersion = /* @__PURE__ */ ((ProtocolVersion2) => {
634
- ProtocolVersion2[ProtocolVersion2["V7"] = 7] = "V7";
635
- ProtocolVersion2[ProtocolVersion2["V8"] = 8] = "V8";
636
- return ProtocolVersion2;
637
- })(ProtocolVersion || {});
638
- var protocolVersionDecoder = enum_(ProtocolVersion).describe(
639
- "Unsupported protocol version"
640
- );
641
-
642
818
  // src/Room.ts
643
819
  import {
644
820
  assertNever as assertNever3,
@@ -646,12 +822,12 @@ import {
646
822
  nodeStreamToCompactNodes,
647
823
  OpCode as OpCode3,
648
824
  raise as raise3,
649
- ServerMsgCode as ServerMsgCode2,
825
+ ServerMsgCode as CoreServerMsgCode,
650
826
  tryParseJson,
651
827
  WebsocketCloseCodes as CloseCode
652
828
  } from "@liveblocks/core";
653
- import { Mutex } from "async-mutex";
654
- import { array as array2, formatInline } from "decoders";
829
+ import { Mutex, tryAcquire } from "async-mutex";
830
+ import { array as array3, formatInline } from "decoders";
655
831
  import { chunked } from "itertools";
656
832
  import { nanoid as nanoid2 } from "nanoid";
657
833
 
@@ -845,16 +1021,21 @@ function hasStaticDataAt(node, key) {
845
1021
  return node.type === CrdtType4.OBJECT && Object.prototype.hasOwnProperty.call(node.data, key) && node.data[key] !== void 0;
846
1022
  }
847
1023
  var InMemoryDriver = class {
1024
+ // Key: `${feedId}:${messageId}`
848
1025
  constructor(options) {
849
1026
  __publicField(this, "_nextActor");
850
1027
  __publicField(this, "_nodes");
851
1028
  __publicField(this, "_metadb");
852
1029
  __publicField(this, "_ydb");
853
1030
  __publicField(this, "_leasedSessions");
1031
+ __publicField(this, "_feeds");
1032
+ __publicField(this, "_feedMessages");
854
1033
  this._nodes = /* @__PURE__ */ new Map();
855
1034
  this._metadb = /* @__PURE__ */ new Map();
856
1035
  this._ydb = /* @__PURE__ */ new Map();
857
1036
  this._leasedSessions = /* @__PURE__ */ new Map();
1037
+ this._feeds = /* @__PURE__ */ new Map();
1038
+ this._feedMessages = /* @__PURE__ */ new Map();
858
1039
  this._nextActor = options?.initialActor ?? -1;
859
1040
  for (const [key, value] of options?.initialNodes ?? []) {
860
1041
  this._nodes.set(key, value);
@@ -894,6 +1075,180 @@ var InMemoryDriver = class {
894
1075
  takeRowsWritten() {
895
1076
  return 0;
896
1077
  }
1078
+ // ---------------------------------------------------------------------------
1079
+ // Feed APIs
1080
+ // ---------------------------------------------------------------------------
1081
+ async list_feeds(options) {
1082
+ const limit = Math.min(options?.limit ?? 20, 100);
1083
+ const since = options?.since;
1084
+ const cursor = options?.cursor;
1085
+ const metadata = options?.metadata;
1086
+ let feeds = [];
1087
+ for (const [_, feed] of this._feeds.entries()) {
1088
+ if (metadata !== void 0) {
1089
+ const feedMetadata = feed.metadata;
1090
+ let matches = true;
1091
+ for (const [key, value] of Object.entries(metadata)) {
1092
+ if (feedMetadata[key] !== value) {
1093
+ matches = false;
1094
+ break;
1095
+ }
1096
+ }
1097
+ if (!matches) continue;
1098
+ }
1099
+ if (since !== void 0 && feed.createdAt < since) {
1100
+ continue;
1101
+ }
1102
+ feeds.push(feed);
1103
+ }
1104
+ feeds.sort((a, b) => {
1105
+ if (b.createdAt !== a.createdAt) {
1106
+ return b.createdAt - a.createdAt;
1107
+ }
1108
+ return b.feedId.localeCompare(a.feedId);
1109
+ });
1110
+ if (cursor !== void 0) {
1111
+ try {
1112
+ const decoded = JSON.parse(
1113
+ decodeURIComponent(
1114
+ escape(atob(cursor.replace(/-/g, "+").replace(/_/g, "/")))
1115
+ )
1116
+ );
1117
+ const [cursorFeedId, cursorCreatedAt] = decoded;
1118
+ feeds = feeds.filter((s) => {
1119
+ return s.createdAt < cursorCreatedAt || s.createdAt === cursorCreatedAt && s.feedId < cursorFeedId;
1120
+ });
1121
+ } catch {
1122
+ }
1123
+ }
1124
+ let nextCursor;
1125
+ if (feeds.length > limit) {
1126
+ feeds = feeds.slice(0, limit);
1127
+ const last = feeds[feeds.length - 1];
1128
+ if (last) {
1129
+ const cursorData = [last.feedId, last.createdAt];
1130
+ nextCursor = btoa(
1131
+ unescape(encodeURIComponent(JSON.stringify(cursorData)))
1132
+ ).replace("/", "_").replace("+", "-").replace(/=+$/, "");
1133
+ }
1134
+ }
1135
+ return { feeds, nextCursor };
1136
+ }
1137
+ async get_feed(feedId) {
1138
+ const feed = this._feeds.get(feedId);
1139
+ if (feed === void 0) {
1140
+ return void 0;
1141
+ }
1142
+ return feed;
1143
+ }
1144
+ async create_feed(feed) {
1145
+ if (this._feeds.has(feed.feedId)) {
1146
+ throw new Error(`Feed ${feed.feedId} already exists`);
1147
+ }
1148
+ this._feeds.set(feed.feedId, {
1149
+ feedId: feed.feedId,
1150
+ metadata: feed.metadata,
1151
+ createdAt: feed.createdAt,
1152
+ updatedAt: feed.updatedAt
1153
+ });
1154
+ }
1155
+ async update_feed_metadata(feedId, metadata) {
1156
+ const existing = this._feeds.get(feedId);
1157
+ if (existing === void 0) {
1158
+ throw new Error(`Feed ${feedId} not found`);
1159
+ }
1160
+ this._feeds.set(feedId, {
1161
+ ...existing,
1162
+ metadata
1163
+ });
1164
+ }
1165
+ async delete_feed(feedId) {
1166
+ const messageKeys = [];
1167
+ for (const [key] of this._feedMessages.entries()) {
1168
+ if (key.startsWith(`${feedId}:`)) {
1169
+ messageKeys.push(key);
1170
+ }
1171
+ }
1172
+ for (const key of messageKeys) {
1173
+ this._feedMessages.delete(key);
1174
+ }
1175
+ this._feeds.delete(feedId);
1176
+ }
1177
+ async list_feed_messages(feedId, options) {
1178
+ const limit = Math.min(options?.limit ?? 20, 100);
1179
+ const since = options?.since;
1180
+ const cursor = options?.cursor;
1181
+ let messages = [];
1182
+ const prefix = `${feedId}:`;
1183
+ for (const [key, message] of this._feedMessages.entries()) {
1184
+ if (key.startsWith(prefix)) {
1185
+ if (since !== void 0 && message.createdAt < since) {
1186
+ continue;
1187
+ }
1188
+ messages.push(message);
1189
+ }
1190
+ }
1191
+ messages.sort((a, b) => {
1192
+ if (b.createdAt !== a.createdAt) {
1193
+ return b.createdAt - a.createdAt;
1194
+ }
1195
+ return b.id.localeCompare(a.id);
1196
+ });
1197
+ if (cursor !== void 0) {
1198
+ try {
1199
+ const decoded = JSON.parse(
1200
+ decodeURIComponent(
1201
+ escape(atob(cursor.replace(/-/g, "+").replace(/_/g, "/")))
1202
+ )
1203
+ );
1204
+ const [cursorMessageId, cursorCreatedAt] = decoded;
1205
+ messages = messages.filter((m) => {
1206
+ return m.createdAt < cursorCreatedAt || m.createdAt === cursorCreatedAt && m.id < cursorMessageId;
1207
+ });
1208
+ } catch {
1209
+ }
1210
+ }
1211
+ let nextCursor;
1212
+ if (messages.length > limit) {
1213
+ messages = messages.slice(0, limit);
1214
+ const last = messages[messages.length - 1];
1215
+ if (last) {
1216
+ const cursorData = [last.id, last.createdAt];
1217
+ nextCursor = btoa(
1218
+ unescape(encodeURIComponent(JSON.stringify(cursorData)))
1219
+ ).replace("/", "_").replace("+", "-").replace(/=+$/, "");
1220
+ }
1221
+ }
1222
+ return { messages, nextCursor };
1223
+ }
1224
+ async add_feed_message(feedId, message) {
1225
+ const feed = this._feeds.get(feedId);
1226
+ if (feed === void 0) {
1227
+ throw new Error(`Feed ${feedId} not found`);
1228
+ }
1229
+ this._feedMessages.set(`${feedId}:${message.id}`, message);
1230
+ }
1231
+ async update_feed_message(feedId, messageId, data, timestamp) {
1232
+ const key = `${feedId}:${messageId}`;
1233
+ const message = this._feedMessages.get(key);
1234
+ if (message === void 0) {
1235
+ throw new Error(`Feed message ${messageId} not found in feed ${feedId}`);
1236
+ }
1237
+ const effectiveTimestamp = timestamp ?? Date.now();
1238
+ if (effectiveTimestamp < message.updatedAt) {
1239
+ return message;
1240
+ }
1241
+ const updatedMessage = {
1242
+ ...message,
1243
+ updatedAt: effectiveTimestamp,
1244
+ data
1245
+ };
1246
+ this._feedMessages.set(key, updatedMessage);
1247
+ return updatedMessage;
1248
+ }
1249
+ async delete_feed_message(feedId, messageId) {
1250
+ this._feedMessages.delete(`${feedId}:${messageId}`);
1251
+ }
897
1252
  next_actor() {
898
1253
  return ++this._nextActor;
899
1254
  }
@@ -1448,7 +1803,7 @@ var YjsStorage = class {
1448
1803
  let encodedTargetVector;
1449
1804
  try {
1450
1805
  encodedTargetVector = stateVector.length > 0 ? Base64.toUint8Array(stateVector) : void 0;
1451
- } catch (e) {
1806
+ } catch {
1452
1807
  logger.warn(
1453
1808
  "Could not get update from passed vector, returning all updates"
1454
1809
  );
@@ -1672,7 +2027,8 @@ function isLeasedSessionExpired(leasedSession) {
1672
2027
  }
1673
2028
 
1674
2029
  // src/Room.ts
1675
- var messagesDecoder = array2(clientMsgDecoder);
2030
+ var messagesDecoder = array3(clientMsgDecoder);
2031
+ var ServerMsgCode2 = { ...CoreServerMsgCode, ...FeedMsgCode };
1676
2032
  var HIGHEST_PROTOCOL_VERSION = Math.max(
1677
2033
  ...Object.values(ProtocolVersion).filter(
1678
2034
  (v) => typeof v === "number"
@@ -1722,6 +2078,7 @@ var BrowserSession = class {
1722
2078
  // Metadata sent to client in ROOM_STATE message's "meta" field
1723
2079
  __privateAdd(this, __socket);
1724
2080
  __privateAdd(this, __debug);
2081
+ /** Updated on every incoming message (including pings) via handleData(). Used for idle timeout detection. */
1725
2082
  __privateAdd(this, __lastActiveAt);
1726
2083
  // We keep a status in-memory in the session of whether we already sent a rejected ops message to the client.
1727
2084
  __privateAdd(this, __hasNotifiedClientStorageUpdateError);
@@ -1733,15 +2090,15 @@ var BrowserSession = class {
1733
2090
  this.publicMeta = ticket.publicMeta;
1734
2091
  __privateSet(this, __socket, socket);
1735
2092
  __privateSet(this, __debug, debug);
1736
- const now = createdAt ?? /* @__PURE__ */ new Date();
2093
+ const now = createdAt?.getTime() ?? Date.now();
1737
2094
  this.createdAt = now;
1738
2095
  __privateSet(this, __lastActiveAt, now);
1739
2096
  __privateSet(this, __hasNotifiedClientStorageUpdateError, false);
1740
2097
  }
1741
2098
  get lastActiveAt() {
1742
2099
  const lastPing = __privateGet(this, __socket).getLastPongTimestamp?.();
1743
- if (lastPing && lastPing > __privateGet(this, __lastActiveAt)) {
1744
- return lastPing;
2100
+ if (lastPing && lastPing.getTime() > __privateGet(this, __lastActiveAt)) {
2101
+ return lastPing.getTime();
1745
2102
  } else {
1746
2103
  return __privateGet(this, __lastActiveAt);
1747
2104
  }
@@ -1749,10 +2106,9 @@ var BrowserSession = class {
1749
2106
  get hasNotifiedClientStorageUpdateError() {
1750
2107
  return __privateGet(this, __hasNotifiedClientStorageUpdateError);
1751
2108
  }
1752
- markActive(now = /* @__PURE__ */ new Date()) {
1753
- if (now > __privateGet(this, __lastActiveAt)) {
1754
- __privateSet(this, __lastActiveAt, now);
1755
- }
2109
+ /** @internal - This should have to be called only from Room, never externally */
2110
+ markActive(now) {
2111
+ __privateSet(this, __lastActiveAt, now ? now.getTime() : Date.now());
1756
2112
  }
1757
2113
  setHasNotifiedClientStorageUpdateError() {
1758
2114
  __privateSet(this, __hasNotifiedClientStorageUpdateError, true);
@@ -1824,6 +2180,15 @@ var Room = class {
1824
2180
  __publicField(this, "meta");
1825
2181
  __publicField(this, "driver");
1826
2182
  __publicField(this, "logger");
2183
+ /**
2184
+ * While a room is in "maintenance mode", all WebSocket connections to the
2185
+ * room should be rejected until it's pulled out of maintenance mode again.
2186
+ * Maintenance mode should only last a couple of milliseconds, typically.
2187
+ *
2188
+ * This mutex ensures that concurrent destructive operations (like
2189
+ * init-storage, storage-reset, etc.) cannot interfere with one another.
2190
+ */
2191
+ __publicField(this, "_maintenanceMode", new Mutex());
1827
2192
  __publicField(this, "_loadData$", null);
1828
2193
  __publicField(this, "_data", null);
1829
2194
  __publicField(this, "_qsize", 0);
@@ -1884,6 +2249,24 @@ var Room = class {
1884
2249
  }
1885
2250
  // prettier-ignore
1886
2251
  // ------------------------------------------------------------------------------------
2252
+ // Maintenance mode
2253
+ // ------------------------------------------------------------------------------------
2254
+ /**
2255
+ * Returns true if the room is currently in maintenance mode.
2256
+ * When in maintenance mode, callers should refuse new WebSocket connections.
2257
+ */
2258
+ get isInMaintenance() {
2259
+ return this._maintenanceMode.isLocked();
2260
+ }
2261
+ /**
2262
+ * Tries to enter maintenance mode and run the given callback exclusively.
2263
+ * If the room is already in maintenance mode, throws E_ALREADY_LOCKED
2264
+ * immediately instead of queuing the request.
2265
+ */
2266
+ async runInMaintenanceMode(callback) {
2267
+ return tryAcquire(this._maintenanceMode).runExclusive(callback);
2268
+ }
2269
+ // ------------------------------------------------------------------------------------
1887
2270
  // Public API
1888
2271
  // ------------------------------------------------------------------------------------
1889
2272
  /**
@@ -2127,6 +2510,7 @@ var Room = class {
2127
2510
  );
2128
2511
  }) {
2129
2512
  const text = typeof data === "string" ? data : raise3("Unsupported message format");
2513
+ this.sessions.get(key)?.markActive();
2130
2514
  if (text === "ping") {
2131
2515
  await this.handlePing(key, ctx);
2132
2516
  } else {
@@ -2340,6 +2724,87 @@ var Room = class {
2340
2724
  await this.deleteLeasedSession(session, ctx, defer);
2341
2725
  }
2342
2726
  }
2727
+ // ---------------------------------------------------------------------------
2728
+ // Feed APIs
2729
+ // ---------------------------------------------------------------------------
2730
+ /**
2731
+ * List feeds with pagination and filtering.
2732
+ */
2733
+ async listFeeds(options) {
2734
+ return await this.driver.list_feeds(options);
2735
+ }
2736
+ /**
2737
+ * Get a specific feed by feed ID.
2738
+ */
2739
+ async getFeed(feedId) {
2740
+ return await this.driver.get_feed(feedId);
2741
+ }
2742
+ /**
2743
+ * Create a new feed.
2744
+ * If timestamp is not provided, current server time is used.
2745
+ */
2746
+ async createFeed(feed) {
2747
+ const now = feed.timestamp ?? Date.now();
2748
+ const fullFeed = {
2749
+ ...feed,
2750
+ createdAt: now,
2751
+ updatedAt: now
2752
+ };
2753
+ await this.driver.create_feed(fullFeed);
2754
+ return fullFeed;
2755
+ }
2756
+ /**
2757
+ * Update a feed's metadata.
2758
+ */
2759
+ async updateFeedMetadata(feedId, metadata) {
2760
+ await this.driver.update_feed_metadata(feedId, metadata);
2761
+ }
2762
+ /**
2763
+ * Delete a feed.
2764
+ */
2765
+ async deleteFeed(feedId) {
2766
+ await this.driver.delete_feed(feedId);
2767
+ }
2768
+ /**
2769
+ * List feed messages for a feed with pagination.
2770
+ */
2771
+ async listFeedMessages(feedId, options) {
2772
+ return await this.driver.list_feed_messages(feedId, options);
2773
+ }
2774
+ /**
2775
+ * Add a message to a feed.
2776
+ * If message id is not provided, a unique ID is automatically generated.
2777
+ * If timestamp is not provided, current server time is used.
2778
+ */
2779
+ async addFeedMessage(feedId, message) {
2780
+ const now = message.timestamp ?? Date.now();
2781
+ const fullMessage = {
2782
+ id: message.id ?? nanoid2(),
2783
+ createdAt: now,
2784
+ updatedAt: now,
2785
+ data: message.data
2786
+ };
2787
+ await this.driver.add_feed_message(feedId, fullMessage);
2788
+ return fullMessage;
2789
+ }
2790
+ /**
2791
+ * Update a feed message's data.
2792
+ * Returns the updated message.
2793
+ */
2794
+ async updateFeedMessage(feedId, messageId, data, timestamp) {
2795
+ return await this.driver.update_feed_message(
2796
+ feedId,
2797
+ messageId,
2798
+ data,
2799
+ timestamp ?? Date.now()
2800
+ );
2801
+ }
2802
+ /**
2803
+ * Delete a feed message.
2804
+ */
2805
+ async deleteFeedMessage(feedId, messageId) {
2806
+ await this.driver.delete_feed_message(feedId, messageId);
2807
+ }
2343
2808
  /**
2344
2809
  * Will send the given ServerMsg through all Session, except the Session
2345
2810
  * where the message originates from.
@@ -2676,6 +3141,176 @@ var Room = class {
2676
3141
  }
2677
3142
  break;
2678
3143
  }
3144
+ case FeedMsgCode.FETCH_FEEDS: {
3145
+ const fetchMsg = msg;
3146
+ const [result, err] = await tryCatch(
3147
+ this.listFeeds({
3148
+ cursor: fetchMsg.cursor,
3149
+ since: fetchMsg.since,
3150
+ limit: fetchMsg.limit,
3151
+ metadata: fetchMsg.metadata
3152
+ })
3153
+ );
3154
+ if (err) {
3155
+ replyImmediately(feedFailureServerMsg(fetchMsg.requestId, err));
3156
+ break;
3157
+ }
3158
+ replyImmediately({
3159
+ type: FeedMsgCode.FEEDS_LIST,
3160
+ requestId: fetchMsg.requestId,
3161
+ feeds: result.feeds,
3162
+ nextCursor: result.nextCursor
3163
+ });
3164
+ break;
3165
+ }
3166
+ case FeedMsgCode.FETCH_FEED_MESSAGES: {
3167
+ const fetchMsg = msg;
3168
+ const [result, err] = await tryCatch(
3169
+ this.listFeedMessages(fetchMsg.feedId, {
3170
+ cursor: fetchMsg.cursor,
3171
+ since: fetchMsg.since,
3172
+ limit: fetchMsg.limit
3173
+ })
3174
+ );
3175
+ if (err) {
3176
+ replyImmediately(feedFailureServerMsg(fetchMsg.requestId, err));
3177
+ break;
3178
+ }
3179
+ replyImmediately({
3180
+ type: FeedMsgCode.FEED_MESSAGES_LIST,
3181
+ requestId: fetchMsg.requestId,
3182
+ feedId: fetchMsg.feedId,
3183
+ messages: result.messages,
3184
+ nextCursor: result.nextCursor
3185
+ });
3186
+ break;
3187
+ }
3188
+ case FeedMsgCode.ADD_FEED: {
3189
+ const addMsg = msg;
3190
+ const [feed, err] = await tryCatch(
3191
+ this.createFeed({
3192
+ feedId: addMsg.feedId,
3193
+ metadata: addMsg.metadata ?? {},
3194
+ timestamp: addMsg.timestamp
3195
+ })
3196
+ );
3197
+ if (err) {
3198
+ replyImmediately(feedFailureServerMsg(addMsg.requestId, err));
3199
+ break;
3200
+ }
3201
+ const feedsMsg = {
3202
+ type: FeedMsgCode.FEEDS_ADDED,
3203
+ feeds: [feed]
3204
+ };
3205
+ replyImmediately(feedsMsg);
3206
+ scheduleFanOut(feedsMsg);
3207
+ break;
3208
+ }
3209
+ case FeedMsgCode.UPDATE_FEED: {
3210
+ const updateMsg = msg;
3211
+ const [, metaErr] = await tryCatch(
3212
+ this.updateFeedMetadata(updateMsg.feedId, updateMsg.metadata)
3213
+ );
3214
+ if (metaErr) {
3215
+ replyImmediately(feedFailureServerMsg(updateMsg.requestId, metaErr));
3216
+ break;
3217
+ }
3218
+ const feed = await this.getFeed(updateMsg.feedId);
3219
+ if (!feed) {
3220
+ replyImmediately(
3221
+ feedRequestFailed(
3222
+ updateMsg.requestId,
3223
+ FeedRequestErrorCode.FEED_NOT_FOUND
3224
+ )
3225
+ );
3226
+ break;
3227
+ }
3228
+ const feedsMsg = {
3229
+ type: FeedMsgCode.FEEDS_UPDATED,
3230
+ feeds: [feed]
3231
+ };
3232
+ replyImmediately(feedsMsg);
3233
+ scheduleFanOut(feedsMsg);
3234
+ break;
3235
+ }
3236
+ case FeedMsgCode.DELETE_FEED: {
3237
+ const deleteMsg = msg;
3238
+ const [, err] = await tryCatch(this.deleteFeed(deleteMsg.feedId));
3239
+ if (err) {
3240
+ replyImmediately(feedFailureServerMsg(deleteMsg.requestId, err));
3241
+ break;
3242
+ }
3243
+ const feedDeletedMsg = {
3244
+ type: FeedMsgCode.FEED_DELETED,
3245
+ feedId: deleteMsg.feedId
3246
+ };
3247
+ replyImmediately(feedDeletedMsg);
3248
+ scheduleFanOut(feedDeletedMsg);
3249
+ break;
3250
+ }
3251
+ case FeedMsgCode.ADD_FEED_MESSAGE: {
3252
+ const addMsg = msg;
3253
+ const [message, err] = await tryCatch(
3254
+ this.addFeedMessage(addMsg.feedId, {
3255
+ data: addMsg.data,
3256
+ id: addMsg.id,
3257
+ timestamp: addMsg.timestamp
3258
+ })
3259
+ );
3260
+ if (err) {
3261
+ replyImmediately(feedFailureServerMsg(addMsg.requestId, err));
3262
+ break;
3263
+ }
3264
+ const feedMessagesMsg = {
3265
+ type: FeedMsgCode.FEED_MESSAGES_ADDED,
3266
+ feedId: addMsg.feedId,
3267
+ messages: [message]
3268
+ };
3269
+ replyImmediately(feedMessagesMsg);
3270
+ scheduleFanOut(feedMessagesMsg);
3271
+ break;
3272
+ }
3273
+ case FeedMsgCode.UPDATE_FEED_MESSAGE: {
3274
+ const updateMsg = msg;
3275
+ const [message, err] = await tryCatch(
3276
+ this.updateFeedMessage(
3277
+ updateMsg.feedId,
3278
+ updateMsg.messageId,
3279
+ updateMsg.data,
3280
+ updateMsg.timestamp
3281
+ )
3282
+ );
3283
+ if (err) {
3284
+ replyImmediately(feedFailureServerMsg(updateMsg.requestId, err));
3285
+ break;
3286
+ }
3287
+ const feedMessagesMsg = {
3288
+ type: FeedMsgCode.FEED_MESSAGES_UPDATED,
3289
+ feedId: updateMsg.feedId,
3290
+ messages: [message]
3291
+ };
3292
+ replyImmediately(feedMessagesMsg);
3293
+ scheduleFanOut(feedMessagesMsg);
3294
+ break;
3295
+ }
3296
+ case FeedMsgCode.DELETE_FEED_MESSAGE: {
3297
+ const deleteMsg = msg;
3298
+ const [, err] = await tryCatch(
3299
+ this.deleteFeedMessage(deleteMsg.feedId, deleteMsg.messageId)
3300
+ );
3301
+ if (err) {
3302
+ replyImmediately(feedFailureServerMsg(deleteMsg.requestId, err));
3303
+ break;
3304
+ }
3305
+ const feedMessagesMsg = {
3306
+ type: FeedMsgCode.FEED_MESSAGES_DELETED,
3307
+ feedId: deleteMsg.feedId,
3308
+ messageIds: [deleteMsg.messageId]
3309
+ };
3310
+ replyImmediately(feedMessagesMsg);
3311
+ scheduleFanOut(feedMessagesMsg);
3312
+ break;
3313
+ }
2679
3314
  default: {
2680
3315
  try {
2681
3316
  return assertNever3(msg, "Unrecognized client msg");
@@ -2692,6 +3327,8 @@ export {
2692
3327
  BrowserSession,
2693
3328
  ConsoleTarget,
2694
3329
  DefaultMap,
3330
+ FeedMsgCode,
3331
+ FeedRequestErrorCode,
2695
3332
  InMemoryDriver,
2696
3333
  LogLevel,
2697
3334
  LogTarget,
@@ -2704,12 +3341,19 @@ export {
2704
3341
  ackIgnoredOp,
2705
3342
  clientMsgDecoder,
2706
3343
  concatUint8Arrays,
3344
+ feedFailureServerMsg,
3345
+ feedMetadataIdDecoder,
3346
+ feedMetadataUpdateDecoder,
3347
+ feedRequestFailed,
3348
+ fetchFeedsMetadataFilterDecoder,
2707
3349
  guidDecoder,
2708
3350
  isLeasedSessionExpired,
2709
3351
  jsonObjectYolo,
2710
3352
  jsonYolo,
2711
3353
  makeInMemorySnapshot,
2712
3354
  makeMetadataDB,
3355
+ mapFeedError,
3356
+ optionalFeedMetadataDecoder,
2713
3357
  plainLsonToNodeStream,
2714
3358
  protocolVersionDecoder,
2715
3359
  quote,