@kispi/chat 0.2.2 → 0.2.3
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.en.md +28 -2
- package/README.md +27 -2
- package/dist/index.cjs +121 -7
- package/dist/index.d.cts +99 -8
- package/dist/index.d.ts +99 -8
- package/dist/index.js +121 -7
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -87,7 +87,7 @@ off()
|
|
|
87
87
|
| `loadOlder({limit?})` | Reads the page before the oldest message (default 100) and **prepends it to `messages`.** Returns `{messages, hasMore}`. Use it when scrolling up. If the list is empty (before subscribing), it reads nothing |
|
|
88
88
|
| `history({before?, after?, limit?, view?})` | Reads the past directly. **Independent of `messages`.** `view` is `'main'`, `'all'`, or `{thread: id}` |
|
|
89
89
|
| `reload()` | Re-reads the latest page |
|
|
90
|
-
| `react(messageId, emoji)` / `unreact(...)` | Reactions. Idempotent.
|
|
90
|
+
| `react(messageId, emoji)` / `unreact(...)` | Reactions. Idempotent. Updates that row's `myReactions` **before the frame goes out**, and rolls it back if the server refuses. Never touches the count |
|
|
91
91
|
| `reactionsOf(messageId, {emoji?, cursor?, limit?})` | Who reacted. **Each row is a (user, emoji) pair**, so one person can appear on several rows. `limit` defaults to 50, max 100; the last page's `cursor` is an empty string |
|
|
92
92
|
| `markRead(seq, {threadId?})` | Read cursor |
|
|
93
93
|
| `typing()` | Typing indicator. Does not throw |
|
|
@@ -139,10 +139,30 @@ the screen froze.
|
|
|
139
139
|
**Reactions: `reactions` is everyone's counts, `myReactions` is the emoji you pressed.**
|
|
140
140
|
The SDK keeps both live — counts from the `count` on `reaction.*` events, `myReactions` from
|
|
141
141
|
those events when their `userId` is `chat.user.id` (including presses from your other tabs and
|
|
142
|
-
devices) and from
|
|
142
|
+
devices) and from your own `react()`/`unreact()`. Both are overwrites, not
|
|
143
143
|
increments, so getting the event and the ack never counts twice. Only the changed row is a new
|
|
144
144
|
object.
|
|
145
145
|
|
|
146
|
+
**`myReactions` moves the moment you press.** `react()`/`unreact()` update that row's
|
|
147
|
+
`myReactions` and emit `messages` **before** the frame goes out, so the press does not feel a
|
|
148
|
+
round trip late — the value is already determined by (you, emoji) and the server is idempotent,
|
|
149
|
+
so the ack has nothing new to say. If the server refuses (a limit, permissions) or the ack never
|
|
150
|
+
arrives (timeout, a dropped connection), the SDK rolls it back — unless something newer has
|
|
151
|
+
taken that spot in the meantime. A later press, a `reaction.*` event carrying your own `userId`,
|
|
152
|
+
and a page from `reload()` or a reconnect all beat a pending optimistic value, and a refusal
|
|
153
|
+
never resurrects a stale one. **The SDK owns this field**: an optimistic layer of your own on
|
|
154
|
+
top of it would give it two owners.
|
|
155
|
+
|
|
156
|
+
**The count (`reactions[].count`) is always the server's.** It is never moved locally. If
|
|
157
|
+
somebody else presses the same emoji while your ack is in flight, a local ±1 disagrees with the
|
|
158
|
+
number the server sends, and the event that would have corrected it has already gone by. Counts
|
|
159
|
+
move on `reaction.*` events only.
|
|
160
|
+
|
|
161
|
+
**Publishing (`send()`) is deliberately not optimistic.** A `before_publish` hook may rewrite
|
|
162
|
+
the body (an emoji filter, a link rewrite), so there is no value on the client to draw ahead of
|
|
163
|
+
time. A message joins the list when its `message.created` arrives. A "sending…" indicator is
|
|
164
|
+
your own view state, and unlike a reaction the SDK cannot decide it for you.
|
|
165
|
+
|
|
146
166
|
**Attach the past to the same list with `loadOlder()`.** `history()` is a direct read that
|
|
147
167
|
doesn't touch the list, so merging its results with the live list, filtering duplicates, and
|
|
148
168
|
handling `reset` and reconnects all fall to the consumer. `loadOlder()` puts those rows into
|
|
@@ -151,6 +171,12 @@ like any other row. Calling it several times concurrently still makes one reques
|
|
|
151
171
|
`hasMore` is `false` you're at the start of the room (or of the retention period).
|
|
152
172
|
`reset` or `reload()` discards the attached past as well.
|
|
153
173
|
|
|
174
|
+
**`hasOlder` and `loadOlder()` describe the SDK's list, not your window.** The SDK never trims
|
|
175
|
+
`room.messages` — if it holds 500 rows, all 500 are there. So if you render a 100-row slice of
|
|
176
|
+
the array you were handed, the other 400 are in `room.messages`, not on the server, and
|
|
177
|
+
`hasOlder === false` is the right answer (it means you are at the start of the room). Widening
|
|
178
|
+
the window is a re-read of `room.messages`; asking the server comes after that.
|
|
179
|
+
|
|
154
180
|
```ts
|
|
155
181
|
showLoadMore(room.hasOlder)
|
|
156
182
|
const { hasMore } = await room.loadOlder()
|
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ off()
|
|
|
87
87
|
| `loadOlder({limit?})` | 가장 오래된 메시지 앞 페이지(기본 100)를 읽어 **`messages` 앞에 붙인다.** `{messages, hasMore}`. 스크롤을 올릴 때 쓴다. 목록이 비어 있으면(구독 전) 아무것도 읽지 않는다 |
|
|
88
88
|
| `history({before?, after?, limit?, view?})` | 과거를 직접 읽는다. **`messages`와 따로 논다.** `view`는 `'main'`, `'all'`, `{thread: id}` |
|
|
89
89
|
| `reload()` | 최근 페이지를 다시 읽는다 |
|
|
90
|
-
| `react(messageId, emoji)` / `unreact(...)` | 리액션. 멱등.
|
|
90
|
+
| `react(messageId, emoji)` / `unreact(...)` | 리액션. 멱등. **프레임보다 먼저** 그 행의 `myReactions`를 고치고, 서버가 거절하면 되돌린다. 개수는 건드리지 않는다 |
|
|
91
91
|
| `reactionsOf(messageId, {emoji?, cursor?, limit?})` | 누가 눌렀는지. **한 행은 (유저, 이모지) 쌍**이라 한 사람이 여러 줄일 수 있다. `limit` 기본 50 최대 100, 마지막 페이지의 `cursor`는 빈 문자열 |
|
|
92
92
|
| `markRead(seq, {threadId?})` | 읽음 커서 |
|
|
93
93
|
| `typing()` | 입력 중. 던지지 않는다 |
|
|
@@ -137,9 +137,27 @@ room.on('messages', m => (messages.value = m))
|
|
|
137
137
|
**리액션: `reactions`는 모두의 개수, `myReactions`는 내가 누른 이모지다.** 둘 다
|
|
138
138
|
SDK가 라이브로 맞춘다 — 개수는 `reaction.*` 이벤트의 `count`로, `myReactions`는 그
|
|
139
139
|
이벤트의 `userId`가 `chat.user.id`일 때(다른 탭·기기에서 누른 것 포함)와 내
|
|
140
|
-
`react()`/`unreact()
|
|
140
|
+
`react()`/`unreact()`로. 둘 다 더하기가 아니라 덮어쓰기라 이벤트와 ack가 모두
|
|
141
141
|
와도 두 번 세지 않는다. 바뀐 행만 새 객체다.
|
|
142
142
|
|
|
143
|
+
**`myReactions`는 누른 즉시 바뀐다.** `react()`/`unreact()`는 프레임을 보내기 **전에**
|
|
144
|
+
그 행의 `myReactions`를 고치고 `messages`를 내보낸다. 그래서 누른 느낌이 왕복만큼
|
|
145
|
+
늦지 않는다 — 누른 값은 (나, 이모지)로 이미 정해져 있고 서버는 멱등이라, ack가
|
|
146
|
+
새로 알려 줄 것이 없다. 서버가 거절하거나(한도, 권한) ack가 못 오면(타임아웃,
|
|
147
|
+
끊김) 되돌린다. 단, 그 사이 더 새로운 값이 자리를 차지했으면 그대로 둔다: 뒤에 누른
|
|
148
|
+
값, 내 `userId`가 실린 `reaction.*` 이벤트, `reload()`나 재접속이 가져온 페이지가
|
|
149
|
+
모두 대기 중인 낙관값을 이긴다. 거절이 낡은 값을 되살리는 일은 없다.
|
|
150
|
+
**이 필드의 주인은 SDK다** — 위에 자기 낙관 계층을 덧대면 주인이 둘이 된다.
|
|
151
|
+
|
|
152
|
+
**개수(`reactions[].count`)는 늘 서버 값이다.** 로컬에서 ±1하지 않는다. ack를
|
|
153
|
+
기다리는 사이 남이 같은 이모지를 누르면 로컬 계산은 서버가 보낼 수와 어긋나고,
|
|
154
|
+
그 차이를 고쳐 줄 이벤트는 이미 지나갔다. 개수는 `reaction.*` 이벤트가 옮긴다.
|
|
155
|
+
|
|
156
|
+
**발행(`send()`)은 일부러 낙관적이지 않다.** `before_publish` 훅이 본문을 바꿀 수
|
|
157
|
+
있어서(이모지 필터, 링크 치환) 미리 그릴 값이 클라이언트에 없다. 메시지는
|
|
158
|
+
`message.created`가 올 때 목록에 들어간다. 보내는 중 표시가 필요하면 그것은 소비자의
|
|
159
|
+
화면 상태이고, 리액션과 달리 SDK가 대신 정해 줄 수 없다.
|
|
160
|
+
|
|
143
161
|
**과거는 `loadOlder()`로 같은 목록에 붙인다.** `history()`는 목록을 건드리지 않는
|
|
144
162
|
직접 읽기라, 그 결과를 라이브 목록과 합치고 중복을 거르고 `reset`과 재접속을
|
|
145
163
|
처리하는 일이 전부 소비자 몫이 된다. `loadOlder()`는 그 행들을 `messages`에 넣으므로
|
|
@@ -147,6 +165,13 @@ SDK가 라이브로 맞춘다 — 개수는 `reaction.*` 이벤트의 `count`로
|
|
|
147
165
|
불러도 요청은 하나이고, `hasMore`가 `false`면 방(또는 보존 기간)의 맨 앞이다.
|
|
148
166
|
`reset`이나 `reload()`는 붙인 과거도 함께 버린다.
|
|
149
167
|
|
|
168
|
+
**`hasOlder`와 `loadOlder()`는 SDK의 목록을 말한다, 당신의 창이 아니라.** SDK는
|
|
169
|
+
`room.messages`를 줄이지 않는다 — 500건을 들고 있으면 500건이 다 거기 있다. 화면
|
|
170
|
+
성능 때문에 받은 배열을 100건으로 잘라 그리고 있다면, 더 보여 줄 400건은 서버가
|
|
171
|
+
아니라 `room.messages`에 있다. 그때 `hasOlder`가 `false`인 것은 맞는 답이다(방의
|
|
172
|
+
맨 앞이라는 뜻이다). 창을 넓히는 일은 `room.messages`를 다시 읽는 것이고, 서버에
|
|
173
|
+
묻는 것은 그 다음이다.
|
|
174
|
+
|
|
150
175
|
```ts
|
|
151
176
|
showLoadMore(room.hasOlder)
|
|
152
177
|
const { hasMore } = await room.loadOlder()
|
package/dist/index.cjs
CHANGED
|
@@ -211,6 +211,18 @@ var Timeline = class {
|
|
|
211
211
|
floor = 1;
|
|
212
212
|
/** A `loadOlder` came back short: the top of what the server keeps was reached. */
|
|
213
213
|
exhausted = false;
|
|
214
|
+
/**
|
|
215
|
+
* Presses this client has made and the server has not answered yet, one
|
|
216
|
+
* entry per (message, emoji) pair, each remembering the last value the
|
|
217
|
+
* **server** gave for that pair.
|
|
218
|
+
*
|
|
219
|
+
* 되돌릴 자리로 "누르기 직전 화면값"을 쓰면 안 된다. 눌렀다 곧바로 취소하고
|
|
220
|
+
* 두 거절이 차례로 오면, 두 번째 거절이 첫 번째의 낙관값 — 서버가 한 번도
|
|
221
|
+
* 가진 적 없는 값 — 을 화면에 되살린다. 마지막으로 서버가 말한 값만이
|
|
222
|
+
* 되돌릴 자리다.
|
|
223
|
+
*/
|
|
224
|
+
presses = /* @__PURE__ */ new Map();
|
|
225
|
+
pressSeq = 0;
|
|
214
226
|
constructor(options) {
|
|
215
227
|
this.options = options;
|
|
216
228
|
}
|
|
@@ -278,6 +290,7 @@ var Timeline = class {
|
|
|
278
290
|
this.epoch++;
|
|
279
291
|
this.fills.clear();
|
|
280
292
|
this.pendingDeletes.clear();
|
|
293
|
+
this.presses.clear();
|
|
281
294
|
this.items = [];
|
|
282
295
|
this.exhausted = false;
|
|
283
296
|
this.shared = false;
|
|
@@ -305,6 +318,7 @@ var Timeline = class {
|
|
|
305
318
|
this.epoch++;
|
|
306
319
|
this.fills.clear();
|
|
307
320
|
const page = [...messages].sort((a, b) => a.seq - b.seq);
|
|
321
|
+
for (const m of page) this.serverRendered(m.id);
|
|
308
322
|
const top = page.at(-1)?.seq ?? 0;
|
|
309
323
|
this.items = [...page, ...this.items.filter((m) => m.seq > top)];
|
|
310
324
|
this.exhausted = false;
|
|
@@ -361,8 +375,57 @@ var Timeline = class {
|
|
|
361
375
|
* cannot drift the way `+1`/`-1` would. `count` undefined leaves the
|
|
362
376
|
* aggregate alone (the react ack path); `mine` undefined leaves
|
|
363
377
|
* `myReactions` alone (somebody else's event).
|
|
378
|
+
*
|
|
379
|
+
* This is the **authoritative** entry point: a value the server sent.
|
|
380
|
+
* So a press still waiting on an answer for the same pair is finished
|
|
381
|
+
* here -- a refusal landing afterwards must not push this value back
|
|
382
|
+
* to the older one it was going to restore.
|
|
364
383
|
*/
|
|
365
384
|
reaction(messageId, emoji, count, mine) {
|
|
385
|
+
if (mine !== void 0) this.presses.delete(pairKey(messageId, emoji));
|
|
386
|
+
this.applyReaction(messageId, emoji, count, mine);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Applies this client's own react/unreact to `myReactions` **before the
|
|
390
|
+
* frame goes out**, and returns the press so its answer can settle it.
|
|
391
|
+
*
|
|
392
|
+
* 누른 값은 (사용자, 이모지)로 로컬에서 정해지고 엔진은 멱등이므로, 미리
|
|
393
|
+
* 적용해도 ack가 새로 알려 줄 것이 없다. `count`는 일부러 건드리지 않는다 —
|
|
394
|
+
* 그 사이 남이 같은 이모지를 누르면 로컬 +1/-1은 서버가 보낼 수와 다르고,
|
|
395
|
+
* 그 차이를 고칠 이벤트는 이미 지나갔다.
|
|
396
|
+
*
|
|
397
|
+
* Undefined when the row is not held: there is nothing to show now and
|
|
398
|
+
* nothing to put back later.
|
|
399
|
+
*/
|
|
400
|
+
pressReaction(messageId, emoji, mine) {
|
|
401
|
+
const at = this.items.findIndex((m) => m.id === messageId);
|
|
402
|
+
if (at === -1) return void 0;
|
|
403
|
+
const key = pairKey(messageId, emoji);
|
|
404
|
+
const base = this.presses.get(key)?.base ?? (this.items[at].myReactions ?? []).includes(emoji);
|
|
405
|
+
const press = { messageId, emoji, mine, seq: ++this.pressSeq };
|
|
406
|
+
this.presses.set(key, { seq: press.seq, base });
|
|
407
|
+
this.applyReaction(messageId, emoji, void 0, mine);
|
|
408
|
+
return press;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Settles a press with what the server said.
|
|
412
|
+
*
|
|
413
|
+
* `accepted` re-asserts the value the press asked for; a refusal puts
|
|
414
|
+
* the last server-given value back. Either way it does nothing once
|
|
415
|
+
* this press no longer owns the pair -- a newer press took it over
|
|
416
|
+
* (and that one's answer decides), or something authoritative already
|
|
417
|
+
* landed on it (a `reaction.*` event carrying this user's id, a
|
|
418
|
+
* `reload()`, a reconnect's catch-up page). Reverting there would
|
|
419
|
+
* resurrect a value older than what is on screen.
|
|
420
|
+
*/
|
|
421
|
+
settleReaction(press, accepted) {
|
|
422
|
+
const key = pairKey(press.messageId, press.emoji);
|
|
423
|
+
const held = this.presses.get(key);
|
|
424
|
+
if (held === void 0 || held.seq !== press.seq) return;
|
|
425
|
+
this.presses.delete(key);
|
|
426
|
+
this.applyReaction(press.messageId, press.emoji, void 0, accepted ? press.mine : held.base);
|
|
427
|
+
}
|
|
428
|
+
applyReaction(messageId, emoji, count, mine) {
|
|
366
429
|
const at = this.items.findIndex((m) => m.id === messageId);
|
|
367
430
|
if (at === -1) return;
|
|
368
431
|
const current = this.items[at];
|
|
@@ -479,6 +542,7 @@ var Timeline = class {
|
|
|
479
542
|
}
|
|
480
543
|
/** Inserts at the seq position, replacing an existing row with that seq. */
|
|
481
544
|
insert(message, notify = true) {
|
|
545
|
+
this.serverRendered(message.id);
|
|
482
546
|
const pending = this.pendingDeletes.has(message.id);
|
|
483
547
|
if (pending) this.pendingDeletes.delete(message.id);
|
|
484
548
|
const withDelete = pending ? emptied(message) : message;
|
|
@@ -503,6 +567,21 @@ var Timeline = class {
|
|
|
503
567
|
this.changed();
|
|
504
568
|
return true;
|
|
505
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Forgets the presses on a row the server has just re-rendered.
|
|
572
|
+
*
|
|
573
|
+
* 히스토리 응답의 행은 뷰어별 `myReactions`를 싣고 오므로 그 값이 서버의
|
|
574
|
+
* 값이다. 누름을 남겨 두면 그 뒤에 온 거절이 더 새로운 진실 위에 옛 값을
|
|
575
|
+
* 덮어쓴다. 행 전체 단위인 것은 페이지가 그 행의 이모지 전부를 다시 그려
|
|
576
|
+
* 주기 때문이다.
|
|
577
|
+
*/
|
|
578
|
+
serverRendered(messageId) {
|
|
579
|
+
if (this.presses.size === 0) return;
|
|
580
|
+
const prefix = `${messageId}\0`;
|
|
581
|
+
for (const key of this.presses.keys()) {
|
|
582
|
+
if (key.startsWith(prefix)) this.presses.delete(key);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
506
585
|
/** Makes `items` safe to mutate: a copy, if the current one was handed out. */
|
|
507
586
|
own() {
|
|
508
587
|
if (!this.shared) return;
|
|
@@ -517,6 +596,9 @@ var Timeline = class {
|
|
|
517
596
|
function emptied(message) {
|
|
518
597
|
return { ...message, body: {}, deletedAt: message.deletedAt ?? Date.now() };
|
|
519
598
|
}
|
|
599
|
+
function pairKey(messageId, emoji) {
|
|
600
|
+
return `${messageId}\0${emoji}`;
|
|
601
|
+
}
|
|
520
602
|
function createTimeline(options) {
|
|
521
603
|
return new Timeline(options);
|
|
522
604
|
}
|
|
@@ -716,17 +798,49 @@ var Room = class extends Emitter {
|
|
|
716
798
|
/**
|
|
717
799
|
* Adds a reaction. Idempotent, like the frame.
|
|
718
800
|
*
|
|
719
|
-
* The
|
|
720
|
-
*
|
|
801
|
+
* The row's `myReactions` moves **before the frame goes out** and rolls
|
|
802
|
+
* back if the server refuses; its count stays the server's. See
|
|
803
|
+
* `toggleReaction`.
|
|
721
804
|
*/
|
|
722
805
|
async react(messageId, emoji) {
|
|
723
|
-
await this.
|
|
724
|
-
this.timeline.reaction(messageId, emoji, void 0, true);
|
|
806
|
+
await this.toggleReaction(messageId, emoji, true);
|
|
725
807
|
}
|
|
726
|
-
/** Removes a reaction. Idempotent
|
|
808
|
+
/** Removes a reaction. Idempotent, and optimistic the same way `react` is. */
|
|
727
809
|
async unreact(messageId, emoji) {
|
|
728
|
-
await this.
|
|
729
|
-
|
|
810
|
+
await this.toggleReaction(messageId, emoji, false);
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Moves `myReactions` now, sends the frame after.
|
|
814
|
+
*
|
|
815
|
+
* 누른 값은 (사용자, 이모지)로 로컬에서 정해지고 엔진은 멱등이므로 ack를
|
|
816
|
+
* 기다릴 이유가 없다 — 기다리면 누른 느낌이 왕복 시간만큼 늦는다. 소비자가
|
|
817
|
+
* 자기 낙관 계층을 덧대면 이 필드의 주인이 둘이 되므로, SDK가 한다.
|
|
818
|
+
* **발행은 다르다**: `before_publish` 훅이 본문을 바꿀 수 있어 미리 그릴 값이
|
|
819
|
+
* 로컬에 없다. 그래서 `send()`는 낙관적이지 않다.
|
|
820
|
+
*
|
|
821
|
+
* 개수는 옮기지 않는다. ack가 오기 전에 남이 같은 이모지를 누르면 로컬
|
|
822
|
+
* +1/-1은 서버가 보낸 수와 다른 값이 되고, 그것을 고칠 이벤트는 이미
|
|
823
|
+
* 지나갔다. 또 ack와 방 프레임은 다른 길로 와 순서가 없으므로, 늦게 온 내
|
|
824
|
+
* ack(count 1)가 앞서 온 남의 이벤트(count 2)를 되돌린다.
|
|
825
|
+
*
|
|
826
|
+
* 거절·타임아웃·끊김이면 되돌리지만, 그 사이 더 새로운 값이 자리를 차지했다면
|
|
827
|
+
* 두고 나온다(`Timeline.settleReaction`).
|
|
828
|
+
*/
|
|
829
|
+
async toggleReaction(messageId, emoji, mine) {
|
|
830
|
+
const press = this.timeline.pressReaction(messageId, emoji, mine);
|
|
831
|
+
try {
|
|
832
|
+
await this.chat.send("react", {
|
|
833
|
+
roomId: await this.resolveId("reacting"),
|
|
834
|
+
messageId,
|
|
835
|
+
emoji,
|
|
836
|
+
op: mine ? "add" : "remove"
|
|
837
|
+
});
|
|
838
|
+
} catch (err) {
|
|
839
|
+
if (press !== void 0) this.timeline.settleReaction(press, false);
|
|
840
|
+
throw err;
|
|
841
|
+
}
|
|
842
|
+
if (press !== void 0) this.timeline.settleReaction(press, true);
|
|
843
|
+
else this.timeline.reaction(messageId, emoji, void 0, mine);
|
|
730
844
|
}
|
|
731
845
|
/**
|
|
732
846
|
* Moves this user's read cursor.
|
package/dist/index.d.cts
CHANGED
|
@@ -146,10 +146,15 @@ type Message = {
|
|
|
146
146
|
* The emoji this client's user has on the message.
|
|
147
147
|
*
|
|
148
148
|
* Filled by history reads and kept in step by the SDK afterwards: a
|
|
149
|
-
* `reaction.*` event whose `userId` is `chat.user.id
|
|
150
|
-
* this client's own `react()`/`unreact()
|
|
151
|
-
* the server
|
|
152
|
-
*
|
|
149
|
+
* `reaction.*` event whose `userId` is `chat.user.id` updates it, and
|
|
150
|
+
* this client's own `react()`/`unreact()` update it **before the frame
|
|
151
|
+
* goes out**, rolling back if the server refuses. Absent on a row the
|
|
152
|
+
* server rendered without a viewer (a live `message.created` has none
|
|
153
|
+
* yet, which is the same as empty).
|
|
154
|
+
*
|
|
155
|
+
* The SDK owns this field, so a consumer does not need -- and should
|
|
156
|
+
* not add -- an optimistic layer of its own on top of it. The count in
|
|
157
|
+
* `reactions` is the opposite: always the server's, never moved locally.
|
|
153
158
|
*/
|
|
154
159
|
myReactions?: string[];
|
|
155
160
|
thread?: {
|
|
@@ -370,12 +375,31 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
370
375
|
/**
|
|
371
376
|
* Adds a reaction. Idempotent, like the frame.
|
|
372
377
|
*
|
|
373
|
-
* The
|
|
374
|
-
*
|
|
378
|
+
* The row's `myReactions` moves **before the frame goes out** and rolls
|
|
379
|
+
* back if the server refuses; its count stays the server's. See
|
|
380
|
+
* `toggleReaction`.
|
|
375
381
|
*/
|
|
376
382
|
react(messageId: string, emoji: string): Promise<void>;
|
|
377
|
-
/** Removes a reaction. Idempotent
|
|
383
|
+
/** Removes a reaction. Idempotent, and optimistic the same way `react` is. */
|
|
378
384
|
unreact(messageId: string, emoji: string): Promise<void>;
|
|
385
|
+
/**
|
|
386
|
+
* Moves `myReactions` now, sends the frame after.
|
|
387
|
+
*
|
|
388
|
+
* 누른 값은 (사용자, 이모지)로 로컬에서 정해지고 엔진은 멱등이므로 ack를
|
|
389
|
+
* 기다릴 이유가 없다 — 기다리면 누른 느낌이 왕복 시간만큼 늦는다. 소비자가
|
|
390
|
+
* 자기 낙관 계층을 덧대면 이 필드의 주인이 둘이 되므로, SDK가 한다.
|
|
391
|
+
* **발행은 다르다**: `before_publish` 훅이 본문을 바꿀 수 있어 미리 그릴 값이
|
|
392
|
+
* 로컬에 없다. 그래서 `send()`는 낙관적이지 않다.
|
|
393
|
+
*
|
|
394
|
+
* 개수는 옮기지 않는다. ack가 오기 전에 남이 같은 이모지를 누르면 로컬
|
|
395
|
+
* +1/-1은 서버가 보낸 수와 다른 값이 되고, 그것을 고칠 이벤트는 이미
|
|
396
|
+
* 지나갔다. 또 ack와 방 프레임은 다른 길로 와 순서가 없으므로, 늦게 온 내
|
|
397
|
+
* ack(count 1)가 앞서 온 남의 이벤트(count 2)를 되돌린다.
|
|
398
|
+
*
|
|
399
|
+
* 거절·타임아웃·끊김이면 되돌리지만, 그 사이 더 새로운 값이 자리를 차지했다면
|
|
400
|
+
* 두고 나온다(`Timeline.settleReaction`).
|
|
401
|
+
*/
|
|
402
|
+
private toggleReaction;
|
|
379
403
|
/**
|
|
380
404
|
* Moves this user's read cursor.
|
|
381
405
|
*
|
|
@@ -847,6 +871,21 @@ type FetchRange = (range: {
|
|
|
847
871
|
before: number;
|
|
848
872
|
limit: number;
|
|
849
873
|
}) => Promise<Message[]>;
|
|
874
|
+
/**
|
|
875
|
+
* A press of this client's own that the server has not answered yet.
|
|
876
|
+
*
|
|
877
|
+
* Carries its own sequence number, which is the whole point: by the time
|
|
878
|
+
* an answer comes back the (message, emoji) pair may belong to a newer
|
|
879
|
+
* press, and settling this one then would put a value nobody is showing
|
|
880
|
+
* any more back on screen.
|
|
881
|
+
*/
|
|
882
|
+
type PendingReaction = {
|
|
883
|
+
messageId: string;
|
|
884
|
+
emoji: string;
|
|
885
|
+
/** What the press asked for: in `myReactions`, or out of it. */
|
|
886
|
+
mine: boolean;
|
|
887
|
+
seq: number;
|
|
888
|
+
};
|
|
850
889
|
type TimelineOptions = {
|
|
851
890
|
fetchRange: FetchRange;
|
|
852
891
|
/** Called whenever the list changes. */
|
|
@@ -897,6 +936,18 @@ declare class Timeline {
|
|
|
897
936
|
private floor;
|
|
898
937
|
/** A `loadOlder` came back short: the top of what the server keeps was reached. */
|
|
899
938
|
private exhausted;
|
|
939
|
+
/**
|
|
940
|
+
* Presses this client has made and the server has not answered yet, one
|
|
941
|
+
* entry per (message, emoji) pair, each remembering the last value the
|
|
942
|
+
* **server** gave for that pair.
|
|
943
|
+
*
|
|
944
|
+
* 되돌릴 자리로 "누르기 직전 화면값"을 쓰면 안 된다. 눌렀다 곧바로 취소하고
|
|
945
|
+
* 두 거절이 차례로 오면, 두 번째 거절이 첫 번째의 낙관값 — 서버가 한 번도
|
|
946
|
+
* 가진 적 없는 값 — 을 화면에 되살린다. 마지막으로 서버가 말한 값만이
|
|
947
|
+
* 되돌릴 자리다.
|
|
948
|
+
*/
|
|
949
|
+
private readonly presses;
|
|
950
|
+
private pressSeq;
|
|
900
951
|
constructor(options: TimelineOptions);
|
|
901
952
|
/** A new array whenever the list changes; never mutated once returned. */
|
|
902
953
|
get messages(): Message[];
|
|
@@ -988,8 +1039,39 @@ declare class Timeline {
|
|
|
988
1039
|
* cannot drift the way `+1`/`-1` would. `count` undefined leaves the
|
|
989
1040
|
* aggregate alone (the react ack path); `mine` undefined leaves
|
|
990
1041
|
* `myReactions` alone (somebody else's event).
|
|
1042
|
+
*
|
|
1043
|
+
* This is the **authoritative** entry point: a value the server sent.
|
|
1044
|
+
* So a press still waiting on an answer for the same pair is finished
|
|
1045
|
+
* here -- a refusal landing afterwards must not push this value back
|
|
1046
|
+
* to the older one it was going to restore.
|
|
991
1047
|
*/
|
|
992
1048
|
reaction(messageId: string, emoji: string, count: number | undefined, mine?: boolean): void;
|
|
1049
|
+
/**
|
|
1050
|
+
* Applies this client's own react/unreact to `myReactions` **before the
|
|
1051
|
+
* frame goes out**, and returns the press so its answer can settle it.
|
|
1052
|
+
*
|
|
1053
|
+
* 누른 값은 (사용자, 이모지)로 로컬에서 정해지고 엔진은 멱등이므로, 미리
|
|
1054
|
+
* 적용해도 ack가 새로 알려 줄 것이 없다. `count`는 일부러 건드리지 않는다 —
|
|
1055
|
+
* 그 사이 남이 같은 이모지를 누르면 로컬 +1/-1은 서버가 보낼 수와 다르고,
|
|
1056
|
+
* 그 차이를 고칠 이벤트는 이미 지나갔다.
|
|
1057
|
+
*
|
|
1058
|
+
* Undefined when the row is not held: there is nothing to show now and
|
|
1059
|
+
* nothing to put back later.
|
|
1060
|
+
*/
|
|
1061
|
+
pressReaction(messageId: string, emoji: string, mine: boolean): PendingReaction | undefined;
|
|
1062
|
+
/**
|
|
1063
|
+
* Settles a press with what the server said.
|
|
1064
|
+
*
|
|
1065
|
+
* `accepted` re-asserts the value the press asked for; a refusal puts
|
|
1066
|
+
* the last server-given value back. Either way it does nothing once
|
|
1067
|
+
* this press no longer owns the pair -- a newer press took it over
|
|
1068
|
+
* (and that one's answer decides), or something authoritative already
|
|
1069
|
+
* landed on it (a `reaction.*` event carrying this user's id, a
|
|
1070
|
+
* `reload()`, a reconnect's catch-up page). Reverting there would
|
|
1071
|
+
* resurrect a value older than what is on screen.
|
|
1072
|
+
*/
|
|
1073
|
+
settleReaction(press: PendingReaction, accepted: boolean): void;
|
|
1074
|
+
private applyReaction;
|
|
993
1075
|
/** Applies a `thread.updated` to the root message's aggregate. */
|
|
994
1076
|
thread(rootId: string, count: number, lastSeq?: number): void;
|
|
995
1077
|
/** Applies a `message.deleted`, remembering it if the row is not here. */
|
|
@@ -1032,10 +1114,19 @@ declare class Timeline {
|
|
|
1032
1114
|
/** Inserts at the seq position, replacing an existing row with that seq. */
|
|
1033
1115
|
private insert;
|
|
1034
1116
|
private applyDelete;
|
|
1117
|
+
/**
|
|
1118
|
+
* Forgets the presses on a row the server has just re-rendered.
|
|
1119
|
+
*
|
|
1120
|
+
* 히스토리 응답의 행은 뷰어별 `myReactions`를 싣고 오므로 그 값이 서버의
|
|
1121
|
+
* 값이다. 누름을 남겨 두면 그 뒤에 온 거절이 더 새로운 진실 위에 옛 값을
|
|
1122
|
+
* 덮어쓴다. 행 전체 단위인 것은 페이지가 그 행의 이모지 전부를 다시 그려
|
|
1123
|
+
* 주기 때문이다.
|
|
1124
|
+
*/
|
|
1125
|
+
private serverRendered;
|
|
1035
1126
|
/** Makes `items` safe to mutate: a copy, if the current one was handed out. */
|
|
1036
1127
|
private own;
|
|
1037
1128
|
private changed;
|
|
1038
1129
|
}
|
|
1039
1130
|
declare function createTimeline(options: TimelineOptions): Timeline;
|
|
1040
1131
|
|
|
1041
|
-
export { type AuthUser, ChatClient, type ChatClientEvents, type ChatClientOptions, ChatError, type ConnectionState, type FetchRange, type Frame, type Hello, type Message, type PublishAck, type ReactionUser, type ReactionsPage, Room, type RoomEvents, type SendInput, type SendOptions, Timeline, type TimelineOptions, type WebSocketFactory, type WebSocketLike, createChatClient, createTimeline };
|
|
1132
|
+
export { type AuthUser, ChatClient, type ChatClientEvents, type ChatClientOptions, ChatError, type ConnectionState, type FetchRange, type Frame, type Hello, type Message, type PendingReaction, type PublishAck, type ReactionUser, type ReactionsPage, Room, type RoomEvents, type SendInput, type SendOptions, Timeline, type TimelineOptions, type WebSocketFactory, type WebSocketLike, createChatClient, createTimeline };
|
package/dist/index.d.ts
CHANGED
|
@@ -146,10 +146,15 @@ type Message = {
|
|
|
146
146
|
* The emoji this client's user has on the message.
|
|
147
147
|
*
|
|
148
148
|
* Filled by history reads and kept in step by the SDK afterwards: a
|
|
149
|
-
* `reaction.*` event whose `userId` is `chat.user.id
|
|
150
|
-
* this client's own `react()`/`unreact()
|
|
151
|
-
* the server
|
|
152
|
-
*
|
|
149
|
+
* `reaction.*` event whose `userId` is `chat.user.id` updates it, and
|
|
150
|
+
* this client's own `react()`/`unreact()` update it **before the frame
|
|
151
|
+
* goes out**, rolling back if the server refuses. Absent on a row the
|
|
152
|
+
* server rendered without a viewer (a live `message.created` has none
|
|
153
|
+
* yet, which is the same as empty).
|
|
154
|
+
*
|
|
155
|
+
* The SDK owns this field, so a consumer does not need -- and should
|
|
156
|
+
* not add -- an optimistic layer of its own on top of it. The count in
|
|
157
|
+
* `reactions` is the opposite: always the server's, never moved locally.
|
|
153
158
|
*/
|
|
154
159
|
myReactions?: string[];
|
|
155
160
|
thread?: {
|
|
@@ -370,12 +375,31 @@ declare class Room extends Emitter<RoomEvents> {
|
|
|
370
375
|
/**
|
|
371
376
|
* Adds a reaction. Idempotent, like the frame.
|
|
372
377
|
*
|
|
373
|
-
* The
|
|
374
|
-
*
|
|
378
|
+
* The row's `myReactions` moves **before the frame goes out** and rolls
|
|
379
|
+
* back if the server refuses; its count stays the server's. See
|
|
380
|
+
* `toggleReaction`.
|
|
375
381
|
*/
|
|
376
382
|
react(messageId: string, emoji: string): Promise<void>;
|
|
377
|
-
/** Removes a reaction. Idempotent
|
|
383
|
+
/** Removes a reaction. Idempotent, and optimistic the same way `react` is. */
|
|
378
384
|
unreact(messageId: string, emoji: string): Promise<void>;
|
|
385
|
+
/**
|
|
386
|
+
* Moves `myReactions` now, sends the frame after.
|
|
387
|
+
*
|
|
388
|
+
* 누른 값은 (사용자, 이모지)로 로컬에서 정해지고 엔진은 멱등이므로 ack를
|
|
389
|
+
* 기다릴 이유가 없다 — 기다리면 누른 느낌이 왕복 시간만큼 늦는다. 소비자가
|
|
390
|
+
* 자기 낙관 계층을 덧대면 이 필드의 주인이 둘이 되므로, SDK가 한다.
|
|
391
|
+
* **발행은 다르다**: `before_publish` 훅이 본문을 바꿀 수 있어 미리 그릴 값이
|
|
392
|
+
* 로컬에 없다. 그래서 `send()`는 낙관적이지 않다.
|
|
393
|
+
*
|
|
394
|
+
* 개수는 옮기지 않는다. ack가 오기 전에 남이 같은 이모지를 누르면 로컬
|
|
395
|
+
* +1/-1은 서버가 보낸 수와 다른 값이 되고, 그것을 고칠 이벤트는 이미
|
|
396
|
+
* 지나갔다. 또 ack와 방 프레임은 다른 길로 와 순서가 없으므로, 늦게 온 내
|
|
397
|
+
* ack(count 1)가 앞서 온 남의 이벤트(count 2)를 되돌린다.
|
|
398
|
+
*
|
|
399
|
+
* 거절·타임아웃·끊김이면 되돌리지만, 그 사이 더 새로운 값이 자리를 차지했다면
|
|
400
|
+
* 두고 나온다(`Timeline.settleReaction`).
|
|
401
|
+
*/
|
|
402
|
+
private toggleReaction;
|
|
379
403
|
/**
|
|
380
404
|
* Moves this user's read cursor.
|
|
381
405
|
*
|
|
@@ -847,6 +871,21 @@ type FetchRange = (range: {
|
|
|
847
871
|
before: number;
|
|
848
872
|
limit: number;
|
|
849
873
|
}) => Promise<Message[]>;
|
|
874
|
+
/**
|
|
875
|
+
* A press of this client's own that the server has not answered yet.
|
|
876
|
+
*
|
|
877
|
+
* Carries its own sequence number, which is the whole point: by the time
|
|
878
|
+
* an answer comes back the (message, emoji) pair may belong to a newer
|
|
879
|
+
* press, and settling this one then would put a value nobody is showing
|
|
880
|
+
* any more back on screen.
|
|
881
|
+
*/
|
|
882
|
+
type PendingReaction = {
|
|
883
|
+
messageId: string;
|
|
884
|
+
emoji: string;
|
|
885
|
+
/** What the press asked for: in `myReactions`, or out of it. */
|
|
886
|
+
mine: boolean;
|
|
887
|
+
seq: number;
|
|
888
|
+
};
|
|
850
889
|
type TimelineOptions = {
|
|
851
890
|
fetchRange: FetchRange;
|
|
852
891
|
/** Called whenever the list changes. */
|
|
@@ -897,6 +936,18 @@ declare class Timeline {
|
|
|
897
936
|
private floor;
|
|
898
937
|
/** A `loadOlder` came back short: the top of what the server keeps was reached. */
|
|
899
938
|
private exhausted;
|
|
939
|
+
/**
|
|
940
|
+
* Presses this client has made and the server has not answered yet, one
|
|
941
|
+
* entry per (message, emoji) pair, each remembering the last value the
|
|
942
|
+
* **server** gave for that pair.
|
|
943
|
+
*
|
|
944
|
+
* 되돌릴 자리로 "누르기 직전 화면값"을 쓰면 안 된다. 눌렀다 곧바로 취소하고
|
|
945
|
+
* 두 거절이 차례로 오면, 두 번째 거절이 첫 번째의 낙관값 — 서버가 한 번도
|
|
946
|
+
* 가진 적 없는 값 — 을 화면에 되살린다. 마지막으로 서버가 말한 값만이
|
|
947
|
+
* 되돌릴 자리다.
|
|
948
|
+
*/
|
|
949
|
+
private readonly presses;
|
|
950
|
+
private pressSeq;
|
|
900
951
|
constructor(options: TimelineOptions);
|
|
901
952
|
/** A new array whenever the list changes; never mutated once returned. */
|
|
902
953
|
get messages(): Message[];
|
|
@@ -988,8 +1039,39 @@ declare class Timeline {
|
|
|
988
1039
|
* cannot drift the way `+1`/`-1` would. `count` undefined leaves the
|
|
989
1040
|
* aggregate alone (the react ack path); `mine` undefined leaves
|
|
990
1041
|
* `myReactions` alone (somebody else's event).
|
|
1042
|
+
*
|
|
1043
|
+
* This is the **authoritative** entry point: a value the server sent.
|
|
1044
|
+
* So a press still waiting on an answer for the same pair is finished
|
|
1045
|
+
* here -- a refusal landing afterwards must not push this value back
|
|
1046
|
+
* to the older one it was going to restore.
|
|
991
1047
|
*/
|
|
992
1048
|
reaction(messageId: string, emoji: string, count: number | undefined, mine?: boolean): void;
|
|
1049
|
+
/**
|
|
1050
|
+
* Applies this client's own react/unreact to `myReactions` **before the
|
|
1051
|
+
* frame goes out**, and returns the press so its answer can settle it.
|
|
1052
|
+
*
|
|
1053
|
+
* 누른 값은 (사용자, 이모지)로 로컬에서 정해지고 엔진은 멱등이므로, 미리
|
|
1054
|
+
* 적용해도 ack가 새로 알려 줄 것이 없다. `count`는 일부러 건드리지 않는다 —
|
|
1055
|
+
* 그 사이 남이 같은 이모지를 누르면 로컬 +1/-1은 서버가 보낼 수와 다르고,
|
|
1056
|
+
* 그 차이를 고칠 이벤트는 이미 지나갔다.
|
|
1057
|
+
*
|
|
1058
|
+
* Undefined when the row is not held: there is nothing to show now and
|
|
1059
|
+
* nothing to put back later.
|
|
1060
|
+
*/
|
|
1061
|
+
pressReaction(messageId: string, emoji: string, mine: boolean): PendingReaction | undefined;
|
|
1062
|
+
/**
|
|
1063
|
+
* Settles a press with what the server said.
|
|
1064
|
+
*
|
|
1065
|
+
* `accepted` re-asserts the value the press asked for; a refusal puts
|
|
1066
|
+
* the last server-given value back. Either way it does nothing once
|
|
1067
|
+
* this press no longer owns the pair -- a newer press took it over
|
|
1068
|
+
* (and that one's answer decides), or something authoritative already
|
|
1069
|
+
* landed on it (a `reaction.*` event carrying this user's id, a
|
|
1070
|
+
* `reload()`, a reconnect's catch-up page). Reverting there would
|
|
1071
|
+
* resurrect a value older than what is on screen.
|
|
1072
|
+
*/
|
|
1073
|
+
settleReaction(press: PendingReaction, accepted: boolean): void;
|
|
1074
|
+
private applyReaction;
|
|
993
1075
|
/** Applies a `thread.updated` to the root message's aggregate. */
|
|
994
1076
|
thread(rootId: string, count: number, lastSeq?: number): void;
|
|
995
1077
|
/** Applies a `message.deleted`, remembering it if the row is not here. */
|
|
@@ -1032,10 +1114,19 @@ declare class Timeline {
|
|
|
1032
1114
|
/** Inserts at the seq position, replacing an existing row with that seq. */
|
|
1033
1115
|
private insert;
|
|
1034
1116
|
private applyDelete;
|
|
1117
|
+
/**
|
|
1118
|
+
* Forgets the presses on a row the server has just re-rendered.
|
|
1119
|
+
*
|
|
1120
|
+
* 히스토리 응답의 행은 뷰어별 `myReactions`를 싣고 오므로 그 값이 서버의
|
|
1121
|
+
* 값이다. 누름을 남겨 두면 그 뒤에 온 거절이 더 새로운 진실 위에 옛 값을
|
|
1122
|
+
* 덮어쓴다. 행 전체 단위인 것은 페이지가 그 행의 이모지 전부를 다시 그려
|
|
1123
|
+
* 주기 때문이다.
|
|
1124
|
+
*/
|
|
1125
|
+
private serverRendered;
|
|
1035
1126
|
/** Makes `items` safe to mutate: a copy, if the current one was handed out. */
|
|
1036
1127
|
private own;
|
|
1037
1128
|
private changed;
|
|
1038
1129
|
}
|
|
1039
1130
|
declare function createTimeline(options: TimelineOptions): Timeline;
|
|
1040
1131
|
|
|
1041
|
-
export { type AuthUser, ChatClient, type ChatClientEvents, type ChatClientOptions, ChatError, type ConnectionState, type FetchRange, type Frame, type Hello, type Message, type PublishAck, type ReactionUser, type ReactionsPage, Room, type RoomEvents, type SendInput, type SendOptions, Timeline, type TimelineOptions, type WebSocketFactory, type WebSocketLike, createChatClient, createTimeline };
|
|
1132
|
+
export { type AuthUser, ChatClient, type ChatClientEvents, type ChatClientOptions, ChatError, type ConnectionState, type FetchRange, type Frame, type Hello, type Message, type PendingReaction, type PublishAck, type ReactionUser, type ReactionsPage, Room, type RoomEvents, type SendInput, type SendOptions, Timeline, type TimelineOptions, type WebSocketFactory, type WebSocketLike, createChatClient, createTimeline };
|
package/dist/index.js
CHANGED
|
@@ -154,6 +154,18 @@ var Timeline = class {
|
|
|
154
154
|
floor = 1;
|
|
155
155
|
/** A `loadOlder` came back short: the top of what the server keeps was reached. */
|
|
156
156
|
exhausted = false;
|
|
157
|
+
/**
|
|
158
|
+
* Presses this client has made and the server has not answered yet, one
|
|
159
|
+
* entry per (message, emoji) pair, each remembering the last value the
|
|
160
|
+
* **server** gave for that pair.
|
|
161
|
+
*
|
|
162
|
+
* 되돌릴 자리로 "누르기 직전 화면값"을 쓰면 안 된다. 눌렀다 곧바로 취소하고
|
|
163
|
+
* 두 거절이 차례로 오면, 두 번째 거절이 첫 번째의 낙관값 — 서버가 한 번도
|
|
164
|
+
* 가진 적 없는 값 — 을 화면에 되살린다. 마지막으로 서버가 말한 값만이
|
|
165
|
+
* 되돌릴 자리다.
|
|
166
|
+
*/
|
|
167
|
+
presses = /* @__PURE__ */ new Map();
|
|
168
|
+
pressSeq = 0;
|
|
157
169
|
constructor(options) {
|
|
158
170
|
this.options = options;
|
|
159
171
|
}
|
|
@@ -221,6 +233,7 @@ var Timeline = class {
|
|
|
221
233
|
this.epoch++;
|
|
222
234
|
this.fills.clear();
|
|
223
235
|
this.pendingDeletes.clear();
|
|
236
|
+
this.presses.clear();
|
|
224
237
|
this.items = [];
|
|
225
238
|
this.exhausted = false;
|
|
226
239
|
this.shared = false;
|
|
@@ -248,6 +261,7 @@ var Timeline = class {
|
|
|
248
261
|
this.epoch++;
|
|
249
262
|
this.fills.clear();
|
|
250
263
|
const page = [...messages].sort((a, b) => a.seq - b.seq);
|
|
264
|
+
for (const m of page) this.serverRendered(m.id);
|
|
251
265
|
const top = page.at(-1)?.seq ?? 0;
|
|
252
266
|
this.items = [...page, ...this.items.filter((m) => m.seq > top)];
|
|
253
267
|
this.exhausted = false;
|
|
@@ -304,8 +318,57 @@ var Timeline = class {
|
|
|
304
318
|
* cannot drift the way `+1`/`-1` would. `count` undefined leaves the
|
|
305
319
|
* aggregate alone (the react ack path); `mine` undefined leaves
|
|
306
320
|
* `myReactions` alone (somebody else's event).
|
|
321
|
+
*
|
|
322
|
+
* This is the **authoritative** entry point: a value the server sent.
|
|
323
|
+
* So a press still waiting on an answer for the same pair is finished
|
|
324
|
+
* here -- a refusal landing afterwards must not push this value back
|
|
325
|
+
* to the older one it was going to restore.
|
|
307
326
|
*/
|
|
308
327
|
reaction(messageId, emoji, count, mine) {
|
|
328
|
+
if (mine !== void 0) this.presses.delete(pairKey(messageId, emoji));
|
|
329
|
+
this.applyReaction(messageId, emoji, count, mine);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Applies this client's own react/unreact to `myReactions` **before the
|
|
333
|
+
* frame goes out**, and returns the press so its answer can settle it.
|
|
334
|
+
*
|
|
335
|
+
* 누른 값은 (사용자, 이모지)로 로컬에서 정해지고 엔진은 멱등이므로, 미리
|
|
336
|
+
* 적용해도 ack가 새로 알려 줄 것이 없다. `count`는 일부러 건드리지 않는다 —
|
|
337
|
+
* 그 사이 남이 같은 이모지를 누르면 로컬 +1/-1은 서버가 보낼 수와 다르고,
|
|
338
|
+
* 그 차이를 고칠 이벤트는 이미 지나갔다.
|
|
339
|
+
*
|
|
340
|
+
* Undefined when the row is not held: there is nothing to show now and
|
|
341
|
+
* nothing to put back later.
|
|
342
|
+
*/
|
|
343
|
+
pressReaction(messageId, emoji, mine) {
|
|
344
|
+
const at = this.items.findIndex((m) => m.id === messageId);
|
|
345
|
+
if (at === -1) return void 0;
|
|
346
|
+
const key = pairKey(messageId, emoji);
|
|
347
|
+
const base = this.presses.get(key)?.base ?? (this.items[at].myReactions ?? []).includes(emoji);
|
|
348
|
+
const press = { messageId, emoji, mine, seq: ++this.pressSeq };
|
|
349
|
+
this.presses.set(key, { seq: press.seq, base });
|
|
350
|
+
this.applyReaction(messageId, emoji, void 0, mine);
|
|
351
|
+
return press;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Settles a press with what the server said.
|
|
355
|
+
*
|
|
356
|
+
* `accepted` re-asserts the value the press asked for; a refusal puts
|
|
357
|
+
* the last server-given value back. Either way it does nothing once
|
|
358
|
+
* this press no longer owns the pair -- a newer press took it over
|
|
359
|
+
* (and that one's answer decides), or something authoritative already
|
|
360
|
+
* landed on it (a `reaction.*` event carrying this user's id, a
|
|
361
|
+
* `reload()`, a reconnect's catch-up page). Reverting there would
|
|
362
|
+
* resurrect a value older than what is on screen.
|
|
363
|
+
*/
|
|
364
|
+
settleReaction(press, accepted) {
|
|
365
|
+
const key = pairKey(press.messageId, press.emoji);
|
|
366
|
+
const held = this.presses.get(key);
|
|
367
|
+
if (held === void 0 || held.seq !== press.seq) return;
|
|
368
|
+
this.presses.delete(key);
|
|
369
|
+
this.applyReaction(press.messageId, press.emoji, void 0, accepted ? press.mine : held.base);
|
|
370
|
+
}
|
|
371
|
+
applyReaction(messageId, emoji, count, mine) {
|
|
309
372
|
const at = this.items.findIndex((m) => m.id === messageId);
|
|
310
373
|
if (at === -1) return;
|
|
311
374
|
const current = this.items[at];
|
|
@@ -422,6 +485,7 @@ var Timeline = class {
|
|
|
422
485
|
}
|
|
423
486
|
/** Inserts at the seq position, replacing an existing row with that seq. */
|
|
424
487
|
insert(message, notify = true) {
|
|
488
|
+
this.serverRendered(message.id);
|
|
425
489
|
const pending = this.pendingDeletes.has(message.id);
|
|
426
490
|
if (pending) this.pendingDeletes.delete(message.id);
|
|
427
491
|
const withDelete = pending ? emptied(message) : message;
|
|
@@ -446,6 +510,21 @@ var Timeline = class {
|
|
|
446
510
|
this.changed();
|
|
447
511
|
return true;
|
|
448
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Forgets the presses on a row the server has just re-rendered.
|
|
515
|
+
*
|
|
516
|
+
* 히스토리 응답의 행은 뷰어별 `myReactions`를 싣고 오므로 그 값이 서버의
|
|
517
|
+
* 값이다. 누름을 남겨 두면 그 뒤에 온 거절이 더 새로운 진실 위에 옛 값을
|
|
518
|
+
* 덮어쓴다. 행 전체 단위인 것은 페이지가 그 행의 이모지 전부를 다시 그려
|
|
519
|
+
* 주기 때문이다.
|
|
520
|
+
*/
|
|
521
|
+
serverRendered(messageId) {
|
|
522
|
+
if (this.presses.size === 0) return;
|
|
523
|
+
const prefix = `${messageId}\0`;
|
|
524
|
+
for (const key of this.presses.keys()) {
|
|
525
|
+
if (key.startsWith(prefix)) this.presses.delete(key);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
449
528
|
/** Makes `items` safe to mutate: a copy, if the current one was handed out. */
|
|
450
529
|
own() {
|
|
451
530
|
if (!this.shared) return;
|
|
@@ -460,6 +539,9 @@ var Timeline = class {
|
|
|
460
539
|
function emptied(message) {
|
|
461
540
|
return { ...message, body: {}, deletedAt: message.deletedAt ?? Date.now() };
|
|
462
541
|
}
|
|
542
|
+
function pairKey(messageId, emoji) {
|
|
543
|
+
return `${messageId}\0${emoji}`;
|
|
544
|
+
}
|
|
463
545
|
function createTimeline(options) {
|
|
464
546
|
return new Timeline(options);
|
|
465
547
|
}
|
|
@@ -659,17 +741,49 @@ var Room = class extends Emitter {
|
|
|
659
741
|
/**
|
|
660
742
|
* Adds a reaction. Idempotent, like the frame.
|
|
661
743
|
*
|
|
662
|
-
* The
|
|
663
|
-
*
|
|
744
|
+
* The row's `myReactions` moves **before the frame goes out** and rolls
|
|
745
|
+
* back if the server refuses; its count stays the server's. See
|
|
746
|
+
* `toggleReaction`.
|
|
664
747
|
*/
|
|
665
748
|
async react(messageId, emoji) {
|
|
666
|
-
await this.
|
|
667
|
-
this.timeline.reaction(messageId, emoji, void 0, true);
|
|
749
|
+
await this.toggleReaction(messageId, emoji, true);
|
|
668
750
|
}
|
|
669
|
-
/** Removes a reaction. Idempotent
|
|
751
|
+
/** Removes a reaction. Idempotent, and optimistic the same way `react` is. */
|
|
670
752
|
async unreact(messageId, emoji) {
|
|
671
|
-
await this.
|
|
672
|
-
|
|
753
|
+
await this.toggleReaction(messageId, emoji, false);
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Moves `myReactions` now, sends the frame after.
|
|
757
|
+
*
|
|
758
|
+
* 누른 값은 (사용자, 이모지)로 로컬에서 정해지고 엔진은 멱등이므로 ack를
|
|
759
|
+
* 기다릴 이유가 없다 — 기다리면 누른 느낌이 왕복 시간만큼 늦는다. 소비자가
|
|
760
|
+
* 자기 낙관 계층을 덧대면 이 필드의 주인이 둘이 되므로, SDK가 한다.
|
|
761
|
+
* **발행은 다르다**: `before_publish` 훅이 본문을 바꿀 수 있어 미리 그릴 값이
|
|
762
|
+
* 로컬에 없다. 그래서 `send()`는 낙관적이지 않다.
|
|
763
|
+
*
|
|
764
|
+
* 개수는 옮기지 않는다. ack가 오기 전에 남이 같은 이모지를 누르면 로컬
|
|
765
|
+
* +1/-1은 서버가 보낸 수와 다른 값이 되고, 그것을 고칠 이벤트는 이미
|
|
766
|
+
* 지나갔다. 또 ack와 방 프레임은 다른 길로 와 순서가 없으므로, 늦게 온 내
|
|
767
|
+
* ack(count 1)가 앞서 온 남의 이벤트(count 2)를 되돌린다.
|
|
768
|
+
*
|
|
769
|
+
* 거절·타임아웃·끊김이면 되돌리지만, 그 사이 더 새로운 값이 자리를 차지했다면
|
|
770
|
+
* 두고 나온다(`Timeline.settleReaction`).
|
|
771
|
+
*/
|
|
772
|
+
async toggleReaction(messageId, emoji, mine) {
|
|
773
|
+
const press = this.timeline.pressReaction(messageId, emoji, mine);
|
|
774
|
+
try {
|
|
775
|
+
await this.chat.send("react", {
|
|
776
|
+
roomId: await this.resolveId("reacting"),
|
|
777
|
+
messageId,
|
|
778
|
+
emoji,
|
|
779
|
+
op: mine ? "add" : "remove"
|
|
780
|
+
});
|
|
781
|
+
} catch (err) {
|
|
782
|
+
if (press !== void 0) this.timeline.settleReaction(press, false);
|
|
783
|
+
throw err;
|
|
784
|
+
}
|
|
785
|
+
if (press !== void 0) this.timeline.settleReaction(press, true);
|
|
786
|
+
else this.timeline.reaction(messageId, emoji, void 0, mine);
|
|
673
787
|
}
|
|
674
788
|
/**
|
|
675
789
|
* Moves this user's read cursor.
|