Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 24 additions & 17 deletions app/lib/encryption/encryption.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Model, Q } from '@nozbe/watermelondb';
import { Q } from '@nozbe/watermelondb';
import EJSON from 'ejson';
import { deleteAsync } from 'expo-file-system/legacy';
import {
Expand Down Expand Up @@ -395,25 +395,32 @@ class Encryption {
sub => sub.lastMessage?.t === E2E_MESSAGE_TYPE && sub.lastMessage?.e2e !== E2E_STATUS.DONE
);

const preparedSubscriptions: (Model | null)[] = await Promise.all(
subsEncryptedToDecrypt.map(async (sub: TSubscriptionModel) => {
const newSub = await this.decryptSubscription(sub);
try {
return sub.prepareUpdate(
protectedFunction((m: TSubscriptionModel) => {
if (newSub?.lastMessage) {
m.lastMessage = newSub.lastMessage;
}
})
);
} catch {
return null;
}
})
const decryptedSubscriptions = await Promise.all(
subsEncryptedToDecrypt.map(async (sub: TSubscriptionModel) => ({
sub,
newSub: await this.decryptSubscription(sub)
}))
);

// Prepare and batch under the writer lock so a concurrent writer can't
// call prepareUpdate on a record with pending changes.
await db.write(async () => {
await db.batch(preparedSubscriptions.filter((record): record is Model => record !== null));
const preparedSubscriptions = decryptedSubscriptions
.map(({ sub, newSub }) => {
try {
return sub.prepareUpdate(
protectedFunction((m: TSubscriptionModel) => {
if (newSub?.lastMessage) {
m.lastMessage = newSub.lastMessage;
}
})
);
Comment on lines +398 to +417

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

mapfile -t files < <(
	fd -t f -e ts -e tsx . app |
		rg '/(encryption|room|rooms(\.test)?)\.(ts|tsx)$'
)

if ((${`#files`[@]} > 0)); then
	rg -n -C 8 \
		'decryptPendingSubscriptions|decryptSubscription|lastMessage|createOrUpdateSubscription|updateLastOpen|prepareUpdate|db\.write|db\.batch' \
		"${files[@]}"
fi

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 224


🏁 Script executed:

#!/usr/bin/env/bash
set -euo pipefail

printf 'Files matching path:\n'
fd -t f -e ts -e tsx '^(encryption|room|rooms(\.test)?)$' app || true

printf '\nTracked files around encryption/room:\n'
git ls-files app | rg 'app/(lib/encryption/(encryption\.ts|.*test.*)|.*room.*\.ts|.*rooms.*\.ts)$' || true

printf '\nSearch relevant symbols:\n'
rg -n -C 6 \
  'decryptPendingSubscriptions|decryptSubscription|lastMessage|createOrUpdateSubscription|updateLastOpen|prepareUpdate|db\.write|db\.batch' app || true

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 50391


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf 'lock file locations:\n'
git ls-files | rg '(^|/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$' || true

printf '\nWatermelonDB version declarations:\n'
for f in $(git ls-files | rg '(^|/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$' | head -20); do
  echo "--- $f"
  rg -n -C 3 '"watermelondb"|watermelondb@|/watermelondb/' "$f" | head -80 || true
done

printf '\nencryption.ts relevant functions:\n'
sed -n '60,120p' app/lib/encryption/encryption.ts
sed -n '380,430p' app/lib/encryption/encryption.ts

printf '\nsubscription model and updates near lastMessage:\n'
git ls-files | rg 'app/.+_subscription.*\.ts$|app/.*subscription.*\.ts$|app/.*model.*\.ts$' | head -50
rg -n -C 8 'class .*Subscription|model.*subscription|lastMessage|updateLastOpen|createOrUpdateSubscription|prepareUpdate' app/lib app/sagas/rooms.js app/actions/room.ts app/actions/rooms.ts | head -220

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 18472


🌐 Web query:

WatermelonDB v0.28.1 db.write prepareUpdate concurrent writes lock writer callbacks

💡 Result:

In WatermelonDB, database writes are strictly serialized to ensure data integrity [1][2]. The library employs a writer-locking mechanism where only one writer (an action defined via database.write() or a @writer method) can execute at any given time [1][2]. Other incoming write requests are queued until the active writer completes [3]. Regarding prepareUpdate and concurrent writes, note the following critical technical constraints: 1. Writer Exclusivity: Because only one writer can be active at once, concurrent write conflicts are prevented at the library level [1][2]. You do not need to implement manual locks for database consistency, as the queueing system manages this automatically [3][2]. 2. Synchronous Batching Requirement: The method prepareUpdate (along with prepareCreate, prepareMarkAsDeleted, etc.) must be executed and passed to database.batch() synchronously within the same writer context [1][4][5]. Performing asynchronous operations (like await) between preparing an update and executing it in batch() can lead to errors such as: "record.prepareUpdate was called on... but wasn't sent to batch synchronously -- this is bad!" [4][6]. 3. Correct Pattern: All logically connected read and write operations should be wrapped together inside a single database.write() block or an @writer method [1][2]. Any necessary asynchronous data fetching or calculations should be completed before preparing the operations, or the batching must occur within the same asynchronous writer block where the preparations are made [4][5]. If you need to nest writer operations, you must use callWriter to ensure the nested operation runs within the existing writer's exclusive lock [1][3]. Nesting writers without callWriter will result in errors or deadlocks because the system expects only one active writer [1].

Citations:


🌐 Web query:

WatermelonDB v0.28.1 Source Database/index.js writer lock prepareUpdate callback

💡 Result:

In WatermelonDB, the prepareUpdate method is used to prepare an update operation on a model record, but it does not execute the update immediately [1]. To ensure consistency and performance, WatermelonDB requires that these prepared operations are executed synchronously within a single database batch transaction [2][1]. If you receive an error stating that record.prepareUpdate was called but was not sent to batch synchronously, it indicates that the prepared update was created inside a writer but not passed to database.batch within the same execution context [2]. Key points regarding the writer lock and prepareUpdate: 1. Writer Locks: WatermelonDB enforces that all database modifications must occur within a Writer [3][4]. Only one Writer can execute at a time to ensure a consistent view of the database [3]. 2. Synchronous Requirement: When you use methods like prepareUpdate or prepareCreate, you are creating a "prepared" record [1]. These must be passed to database.batch or writer.batch synchronously [2][1]. 3. Correct Usage: You should perform your prepare calls inside the same writer block where you call the batch function [2]. Example of correct usage: await database.write(async => { const record = await collection.find('id'); const preparedUpdate = record.prepareUpdate(model => { model.field = 'new value'; }); await database.batch(preparedUpdate); }); If you perform asynchronous operations (e.g., fetching other data) between preparing an update and batching it, you risk breaking the synchronous requirement [2]. All necessary data should be prepared or fetched before the final batch execution [5].

Citations:


🌐 Web query:

@nozbe/watermelondb 0.28.1-0 GitHub source write writer lock pendingQueue changeRecords

💡 Result:

The version 0.28.1-0 of @nozbe/watermelondb was released on July 24, 2025 [1][2]. This release is primarily a version bump [1] and does not contain specific changes to the underlying architecture of writer locks, the pending queue, or changeRecords handling. In WatermelonDB, the core concurrency model remains unchanged in this version: 1. Writer Locks and PendingQueue: WatermelonDB enforces a strict concurrency model where only one writer can execute at a time [3]. When multiple write operations are initiated, they are placed in a pending queue [4]. If a writer is already active, subsequent writers are queued [4]. A common diagnostic warning occurs when many writers are queued, which can occur if operations are not batched efficiently [4][5]. 2. ChangeRecords: The concept of changeRecords is intrinsic to WatermelonDB's reactivity and synchronization system, where database mutations (create, update, delete) are tracked to propagate changes to observers and the synchronization engine [6]. 3. Development Recommendations: To avoid issues with writer locks and pending queues, the official documentation recommends batching database writes using database.batch rather than executing many individual writers [3][5]. For nested operations, the library requires the use of callWriter or callReader to manage context correctly within existing transactions [3][4]. For detailed implementation logic or migration guides, developers should refer to the standard documentation for the 0.28.x release series, as no breaking structural changes were introduced in 0.28.1-0 [7][8].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf 'Find lock-creation/update paths targeting encrypted subscription lastMessage:\n'
rg -n -C 5 \
  'lastMessage|lastMessageTs|updateLastOpen|createOrUpdateSubscription|prepareCreate|Object\.assign|subscription\.lastMessage|e2e_key_id|encrypted' \
  app/lib app/sagas/rooms.js app/actions/room.ts app/actions/rooms.ts \
  | rg -C 4 'lastMessage|updateLastOpen|createOrUpdateSubscription|Object\.assign|prepareCreate|e2e_key_id|encrypted' \
  | head -280

printf '\nList subscription-related tracked files:\n'
git ls-files app | rg 'subscription|room|rooms|subscriptions' | head -120

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 16619


🌐 Web query:

"Nozbe/WatermelonDB" "v0.28.1-0" "src/Database/index.js" "writer" "pendingQueue"

💡 Result:

In WatermelonDB, including version 0.28.1-0, database operations are managed by a queuing system to ensure data consistency [1][2]. The code mentioned in src/Database/index.js interacts with an ActionQueue (or WorkQueue in later versions) which handles the scheduling of readers and writers [1][3]. The pending queue system functions as follows: 1. Consistency and Locking: WatermelonDB enforces that only one writer can execute at a time [2]. All write operations (create, update, delete) must occur within a writer block [1][2]. When a write operation is initiated, it is added to a pending queue if another action is currently running [1][2]. 2. Handling Queued Actions: If multiple write operations are triggered simultaneously, they are placed in a queue to be executed sequentially [4]. The library provides warnings if a large number of actions accumulate in this queue, as this may indicate that previous actions are stuck or that the application is inefficiently dispatching individual writes instead of using batching [5][4]. 3. Nesting: If you need to perform a write operation from within another writer, you must use the callWriter method [2]. Direct execution of a nested writer will fail because the database lock is already held by the parent writer [2]. 4. Internal Mechanics: Internally, the Database class maintains an instance of the queue (often referenced as _actionQueue or _workQueue in the source) [1][3]. Methods like unsafeResetDatabase specifically trigger an abort of all pending actions to ensure a clean state during reset operations [1]. If you are encountering issues related to a large number of writers in the queue, the recommended practice is to aggregate changes into a single batch operation using db.batch instead of executing multiple individual writes [4][2].

Citations:


Guard against stale decrypted subscription snapshots.

decryptSubscription(lastMessage) runs before db.write. If another writer changes the same subscription between decryption and prepareUpdate, this callback can overwrite the newer lastMessage with the older decrypted value.

Capture the source last-message identity or timestamp before decryption. Validate it against the current record inside prepareUpdate; if it changed, re-read and decrypt the current record or skip and schedule a retry. Add a regression test for a newer lastMessage arriving during delayed subscription decryption.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/encryption/encryption.ts` around lines 398 - 417, Update the
subscription flow around decryptSubscription and prepareUpdate to prevent stale
decrypted snapshots from overwriting newer lastMessage values: capture the
source last-message identity or timestamp before decryption, validate it against
the current record inside prepareUpdate, and when it differs re-read and decrypt
the current subscription or skip it and schedule a retry. Add a regression test
covering a newer lastMessage arriving while decryption is delayed.

} catch {
return null;
}
})
.filter((record): record is TSubscriptionModel => record !== null);
await db.batch(preparedSubscriptions);
});
} catch (e) {
log(e);
Expand Down
174 changes: 174 additions & 0 deletions app/lib/methods/subscriptions/rooms.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { createOrUpdateSubscription } from './rooms';
import { updateLastOpen } from '../updateLastOpen';
import { getSubscriptionByRoomId } from '../../database/services/Subscription';
import { getMessageById } from '../../database/services/Message';
import log from '../helpers/log';

jest.mock('../../services/sdk', () => ({
__esModule: true,
default: {}
}));

jest.mock('../../store/auxStore', () => ({
store: {
getState: jest.fn(() => ({ room: { subscribedRoom: null } })),
dispatch: jest.fn()
}
}));

jest.mock('../helpers/log', () => ({
__esModule: true,
default: jest.fn()
}));

jest.mock('../helpers', () => ({
getRoomAvatar: jest.fn(),
getRoomTitle: jest.fn(),
getSenderName: jest.fn(),
random: jest.fn()
}));

jest.mock('../helpers/protectedFunction', () => ({
__esModule: true,
default: (fn: (...args: unknown[]) => unknown) => fn
}));

jest.mock('../helpers/buildMessage', () => ({
__esModule: true,
default: (msg: unknown) => msg
}));

jest.mock('../helpers/mergeSubscriptionsRooms', () => ({
merge: (subscription: unknown) => subscription
}));

jest.mock('../../encryption', () => ({
Encryption: {
decryptPendingSubscriptions: jest.fn(),
decryptPendingMessages: jest.fn(),
getRoomInstance: jest.fn()
}
}));

jest.mock('../updateMessages', () => ({
__esModule: true,
default: jest.fn()
}));

jest.mock('../getRoom', () => ({
getRoom: jest.fn()
}));

jest.mock('../actions', () => ({
handlePayloadUserInteraction: jest.fn()
}));

jest.mock('../../../actions/room', () => ({
removedRoom: jest.fn()
}));

jest.mock('../../../actions/login', () => ({
setUser: jest.fn()
}));

jest.mock('../../../actions/videoConf', () => ({
handleVideoConfIncomingWebsocketMessages: jest.fn()
}));

jest.mock('../../../containers/InAppNotification', () => ({
INAPP_NOTIFICATION_EMITTER: 'NotificationInApp'
}));

const mockDbBatch = jest.fn();
jest.mock('../../database', () => {
let writerQueue: Promise<unknown> = Promise.resolve();
const mockCollection = {
find: jest.fn(() => Promise.reject(new Error('not found'))),
prepareCreate: jest.fn(() => ({})),
schema: {}
};
return {
__esModule: true,
default: {
active: {
get: () => mockCollection,
write: jest.fn((callback: () => Promise<void>) => {
const run = writerQueue.then(() => callback());
writerQueue = run.catch(() => undefined);
return run;
}),
batch: (...args: unknown[]) => mockDbBatch(...args)
}
}
};
});

jest.mock('../../database/services/Subscription', () => ({
getSubscriptionByRoomId: jest.fn()
}));

jest.mock('../../database/services/Message', () => ({
getMessageById: jest.fn()
}));

const rid = 'GENERAL';

// Mimics a WatermelonDB Model: one cached instance per record, and
// prepareUpdate throws while a previous prepared update is not committed.
const makeSubscriptionRecord = () => {
const record: any = {
rid,
lastOpen: null,
_preparedState: null as string | null,
prepareUpdate(recordUpdater: (s: any) => void) {
if (record._preparedState) {
throw new Error(`Cannot update a record with pending changes (subscriptions#${rid})`);
}
recordUpdater(record);
record._preparedState = 'update';
return record;
},
update(recordUpdater: (s: any) => void) {
record.prepareUpdate(recordUpdater);
record._preparedState = null;
return Promise.resolve(record);
}
};
return record;
};

describe('createOrUpdateSubscription concurrency', () => {
beforeEach(() => {
jest.clearAllMocks();
mockDbBatch.mockImplementation((batch: any[]) => {
(Array.isArray(batch) ? batch : [batch]).forEach(item => {
if (item && typeof item === 'object' && '_preparedState' in item) {
item._preparedState = null;
}
});
return Promise.resolve(undefined);
});
});

it('does not leave a prepared subscription visible to a concurrent updateLastOpen', async () => {
const record = makeSubscriptionRecord();
(getSubscriptionByRoomId as jest.Mock).mockResolvedValue(record);
// Slow message lookup keeps createOrUpdateSubscription busy after it fetched the subscription.
(getMessageById as jest.Mock).mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(null), 10)));

const subscription = {
rid,
_id: rid,
lastMessage: { _id: 'msg-id', rid, msg: 'hi' }
} as any;

await Promise.all([
createOrUpdateSubscription(subscription, undefined as any),
updateLastOpen(rid, [{ _updatedAt: '2026-01-01T12:00:00.000Z' }])
]);

const loggedPendingChanges = (log as jest.Mock).mock.calls.some(([error]) => /pending changes/.test(error?.message));
expect(loggedPendingChanges).toBe(false);
expect(record.lastOpen).toEqual(new Date('2026-01-01T12:00:00.000Z'));
});
Comment on lines +153 to +173

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether the new test fails on the pre-fix implementation of createOrUpdateSubscription.
set -euo pipefail

fd -t f 'rooms.ts' app/lib/methods/subscriptions --exec sh -c '
  echo "=== $1 (current) ==="
  rg -n "db.write|prepareUpdate|prepareCreate|db.batch|getSubscriptionByRoomId|getMessageById" "$1"
' _ {}

echo "=== previous revision of rooms.ts ==="
git log --oneline -3 -- app/lib/methods/subscriptions/rooms.ts
git show HEAD~1:app/lib/methods/subscriptions/rooms.ts 2>/dev/null \
  | rg -n "db.write|prepareUpdate|prepareCreate|db.batch|getSubscriptionByRoomId|getMessageById" || echo "previous revision not available"

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2005


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== current implementation ==="
sed -n '150,228p' app/lib/methods/subscriptions/rooms.ts

echo "=== previous implementation ==="
git show HEAD~1:app/lib/methods/subscriptions/rooms.ts | sed -n '150,228p'

echo "=== test ==="
sed -n '130,180p' app/lib/methods/subscriptions/rooms.test.ts

echo "=== mockDbBatch / db.write definitions in test file ==="
rg -n "mockDbBatch|db\.write|createOrUpdateSubscription|updateLastOpen" app/lib/methods/subscriptions/rooms.test.ts

echo "=== imports and setup ==="
sed -n '1,80p' app/lib/methods/subscriptions/rooms.test.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 8595


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import asyncio

class MockDb:
    def __init__(self):
        self.lock = None
        self.batch_calls = []
    async def write(self, fn):
        while self.lock is not None:
            await asyncio.sleep(0)
        self.lock = True
        try:
            return await fn()
        finally:
            self.lock = None
    def batch(self, batch):
        self.batch_calls.append(batch); return asyncio.sleep(0)

class SubscriptionRecord:
    def __init__(self):
        self._preparedState = 'pending'
        self.lastOpen = 'old'
    def prepareUpdate(self, update):
        if self._preparedState is None:
            raise Exception('RecordAlreadyPrepared: pending changes')
        update(self)
        return self
    def update(self, data):
        self.lastOpen = data.get('lastOpen', self.lastOpen)

async def update_last_open_old(db, rid, records):
    sub = SubscriptionRecord()
    await db.write(lambda: (
        db.batch(sub.prepareUpdate(lambda s: s.update(records[0]))),
    )[0])
    return sub.lastOpen

async def create_or_update_old(db, rid, msg_delay=False):
    sub = SubscriptionRecord()
    batch = []
    batch.append(sub.prepareUpdate(lambda s: s.update({'lastMessage': {'_id': 'msg-id'}})))
    if msg_delay:
        await asyncio.sleep(0.01)
    await db.write(lambda: db.batch(batch))

async def main():
    result = []
    for name, cfunc in [('old', create_or_update_old), ('old', update_last_open_old)]:
        pass

for name, cfunc in [('createOrUpdateSubscription_old', create_or_update_old), ('updateLastOpen_old', update_last_open_old)]:
    print(name)
    async def run():
        db = MockDb()
        try:
            await asyncio.gather(cfunc(db, 'rid'), update_last_open_old(db, 'rid', [{'lastOpen': 'new'}]))
            print('ok')
        except Exception as e:
            print(f'raised: {e}')
    asyncio.run(run())

print('non-deterministic interleaving example: updateLastOpen acquires writer before createOrUpdateSubscription prepares')
async def run():
    db = MockDb()
    sub = SubscriptionRecord()
    batch = []
    batch.append(sub.prepareUpdate(lambda s: s.update({'lastMessage': {'_id': 'msg-id'}})))
    await asyncio.sleep(0)
    await db.write(lambda: db.batch(batch)).get() if hasattr((tmp := db.write(lambda: db.batch(batch))).get, '__await__') else None
PY

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 420


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== updateLastOpen implementation / mocks ==="
rg -n "function updateLastOpen|const updateLastOpen|updateLastOpen|db\.write|prepareUpdate|_preparedState" app/lib -g '*.ts' -g '*.tsx'

echo "=== focused updateLastOpen files ==="
fd -t f '^updateLastOpen\.(ts|tsx)$' app/lib
while IFS= read -r f; do
  echo "=== $f ==="
  sed -n '1,220p' "$f"
done < <(fd -t f '^updateLastOpen\.(ts|tsx)$' app/lib)

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 11238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== updateLastOpen implementation ==="
sed -n '1,80p' app/lib/methods/updateLastOpen.ts

echo "=== Subscription service implementation ==="
sed -n '1,180p' app/lib/database/services/Subscription.ts | sed -n '1,160p'

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2515


Make the concurrency ordering explicit.

updateLastOpen awaits getSubscriptionByRoomId(rid) before entering db.write, while this test resolves that lookup immediately. updateLastOpen can acquire and finish inside the writer lock before createOrUpdateSubscription performs any pre-write preparation, so the existing arrangement may pass even on the old implementation. Delay the subscription lookup so createOrUpdateSubscription prepares while updateLastOpen is inside its writer lock, and keep the positive mockDbBatch assertion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/methods/subscriptions/rooms.test.ts` around lines 153 - 173, The
concurrency test should force updateLastOpen to hold the writer lock while
createOrUpdateSubscription completes its preparation. In the test around
createOrUpdateSubscription and updateLastOpen, delay the getSubscriptionByRoomId
mock until the intended ordering is established, and retain a positive assertion
that mockDbBatch was called to verify the writer path executed.

});
123 changes: 63 additions & 60 deletions app/lib/methods/subscriptions/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const WINDOW_TIME = 500;

export let roomsSubscription: { stop: () => void } | null = null;

const createOrUpdateSubscription = async (subscription: ISubscription, room: IServerRoom | IRoom) => {
export const createOrUpdateSubscription = async (subscription: ISubscription, room: IServerRoom | IRoom) => {
try {
const db = database.active;
const subCollection = db.get('subscriptions');
Expand Down Expand Up @@ -150,74 +150,77 @@ const createOrUpdateSubscription = async (subscription: ISubscription, room: ISe
}

const tmp = merge(subscription, room);
const sub = await getSubscriptionByRoomId(tmp.rid);

const batch: Model[] = [];
if (sub) {
try {
const update = sub.prepareUpdate(s => {
Object.assign(s, tmp);
if (subscription.announcement) {
if (subscription.announcement !== sub.announcement) {
s.bannerClosed = false;
// Serialize the fetch, prepares and the batch under the writer lock so a concurrent
// writer can't call prepareUpdate on a record with pending changes.
await db.write(async () => {
const sub = await getSubscriptionByRoomId(tmp.rid);

const batch: Model[] = [];
if (sub) {
try {
const update = sub.prepareUpdate(s => {
Object.assign(s, tmp);
if (subscription.announcement) {
if (subscription.announcement !== sub.announcement) {
s.bannerClosed = false;
}
}
}
if (sub.hideUnreadStatus && subscription.hasOwnProperty('hideUnreadStatus')) {
if (sub.hideUnreadStatus !== subscription.hideUnreadStatus) {
s.hideUnreadStatus = !!subscription.hideUnreadStatus;
if (sub.hideUnreadStatus && subscription.hasOwnProperty('hideUnreadStatus')) {
if (sub.hideUnreadStatus !== subscription.hideUnreadStatus) {
s.hideUnreadStatus = !!subscription.hideUnreadStatus;
}
}
}
});
batch.push(update);
} catch (e) {
console.log(e);
}
} else {
try {
const create = subCollection.prepareCreate(s => {
s._raw = sanitizedRaw({ id: tmp.rid }, subCollection.schema);
Object.assign(s, tmp);
if (s.roomUpdatedAt) {
s.roomUpdatedAt = new Date();
}
});
batch.push(create);
} catch (e) {
console.log(e);
});
batch.push(update);
} catch (e) {
console.log(e);
}
} else {
try {
const create = subCollection.prepareCreate(s => {
s._raw = sanitizedRaw({ id: tmp.rid }, subCollection.schema);
Object.assign(s, tmp);
if (s.roomUpdatedAt) {
s.roomUpdatedAt = new Date();
}
});
batch.push(create);
} catch (e) {
console.log(e);
}
}
}

const { subscribedRoom } = store.getState().room;
if (tmp.lastMessage && subscribedRoom !== tmp.rid) {
const lastMessage = buildMessage(tmp.lastMessage);
const messagesCollection = db.get('messages');
let messageRecord = {} as TMessageModel | null;
if (lastMessage) {
messageRecord = await getMessageById(lastMessage._id);
}
const { subscribedRoom } = store.getState().room;
if (tmp.lastMessage && subscribedRoom !== tmp.rid) {
const lastMessage = buildMessage(tmp.lastMessage);
const messagesCollection = db.get('messages');
let messageRecord = {} as TMessageModel | null;
if (lastMessage) {
messageRecord = await getMessageById(lastMessage._id);
}

if (messageRecord) {
batch.push(
messageRecord.prepareUpdate(() => {
Object.assign(messageRecord, lastMessage);
})
);
} else {
batch.push(
messagesCollection.prepareCreate(m => {
if (lastMessage) {
m._raw = sanitizedRaw({ id: lastMessage._id }, messagesCollection.schema);
if (m.subscription) {
m.subscription.id = lastMessage.rid;
if (messageRecord) {
batch.push(
messageRecord.prepareUpdate(() => {
Object.assign(messageRecord, lastMessage);
})
);
} else {
batch.push(
messagesCollection.prepareCreate(m => {
if (lastMessage) {
m._raw = sanitizedRaw({ id: lastMessage._id }, messagesCollection.schema);
if (m.subscription) {
m.subscription.id = lastMessage.rid;
}
}
}
return Object.assign(m, lastMessage);
})
);
return Object.assign(m, lastMessage);
})
);
}
}
}

await db.write(async () => {
await db.batch(batch);
});

Expand Down
Loading