@arken/node 1.5.0 → 1.5.2

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.
Files changed (160) hide show
  1. package/build/modules/character/character.service.js.map +1 -1
  2. package/build/modules/chat/chat.service.js.map +1 -1
  3. package/build/modules/core/core.models.js.map +1 -1
  4. package/build/modules/core/core.service.js.map +1 -1
  5. package/build/modules/profile/profile.service.js.map +1 -1
  6. package/build/package.json +2 -2
  7. package/build/tsconfig.tsbuildinfo +1 -1
  8. package/build/types.d.ts +1 -0
  9. package/build/types.js +1 -0
  10. package/build/types.js.map +1 -1
  11. package/build/util/mongo.js.map +1 -1
  12. package/db.ts +76 -1
  13. package/index.ts +351 -18
  14. package/{util/mongo.ts → mongo.ts} +2 -0
  15. package/package.json +3 -3
  16. package/tsconfig.json +33 -2
  17. package/types.ts +2 -0
  18. package/util.ts +1 -0
  19. package/modules/area/area.models.ts +0 -15
  20. package/modules/area/area.router.ts +0 -74
  21. package/modules/area/area.schema.ts +0 -22
  22. package/modules/area/area.service.ts +0 -124
  23. package/modules/area/area.types.ts +0 -26
  24. package/modules/area/index.ts +0 -5
  25. package/modules/asset/asset.models.ts +0 -59
  26. package/modules/asset/asset.router.ts +0 -55
  27. package/modules/asset/asset.schema.ts +0 -27
  28. package/modules/asset/asset.service.ts +0 -85
  29. package/modules/asset/asset.types.ts +0 -22
  30. package/modules/asset/index.ts +0 -5
  31. package/modules/chain/chain.models.ts +0 -50
  32. package/modules/chain/chain.router.ts +0 -104
  33. package/modules/chain/chain.schema.ts +0 -52
  34. package/modules/chain/chain.service.ts +0 -167
  35. package/modules/chain/chain.types.ts +0 -24
  36. package/modules/chain/index.ts +0 -5
  37. package/modules/character/character.models.ts +0 -174
  38. package/modules/character/character.router.ts +0 -314
  39. package/modules/character/character.schema.ts +0 -147
  40. package/modules/character/character.service.ts +0 -875
  41. package/modules/character/character.types.ts +0 -64
  42. package/modules/character/index.ts +0 -5
  43. package/modules/chat/chat.models.ts +0 -43
  44. package/modules/chat/chat.router.ts +0 -67
  45. package/modules/chat/chat.schema.ts +0 -36
  46. package/modules/chat/chat.service.ts +0 -120
  47. package/modules/chat/chat.types.ts +0 -20
  48. package/modules/chat/index.ts +0 -5
  49. package/modules/collection/collection.models.ts +0 -76
  50. package/modules/collection/collection.router.ts +0 -91
  51. package/modules/collection/collection.schema.ts +0 -90
  52. package/modules/collection/collection.service.ts +0 -192
  53. package/modules/collection/collection.types.ts +0 -36
  54. package/modules/collection/index.ts +0 -5
  55. package/modules/core/core.models.ts +0 -1379
  56. package/modules/core/core.router.ts +0 -1781
  57. package/modules/core/core.schema.ts +0 -847
  58. package/modules/core/core.service.ts +0 -2822
  59. package/modules/core/core.types.ts +0 -340
  60. package/modules/core/index.ts +0 -5
  61. package/modules/core/mail/applyPatchesOrMail.ts +0 -568
  62. package/modules/core/mail/mailClaimablePatchesBatch.ts +0 -381
  63. package/modules/game/game.models.ts +0 -53
  64. package/modules/game/game.router.ts +0 -110
  65. package/modules/game/game.schema.ts +0 -23
  66. package/modules/game/game.service.ts +0 -143
  67. package/modules/game/game.types.ts +0 -28
  68. package/modules/game/index.ts +0 -5
  69. package/modules/interface/index.ts +0 -5
  70. package/modules/interface/interface.canonicalize.ts +0 -279
  71. package/modules/interface/interface.models.ts +0 -40
  72. package/modules/interface/interface.router.ts +0 -175
  73. package/modules/interface/interface.schema.ts +0 -59
  74. package/modules/interface/interface.service.ts +0 -356
  75. package/modules/interface/interface.types.ts +0 -25
  76. package/modules/item/index.ts +0 -5
  77. package/modules/item/item.models.ts +0 -124
  78. package/modules/item/item.router.ts +0 -103
  79. package/modules/item/item.schema.ts +0 -120
  80. package/modules/item/item.service.ts +0 -167
  81. package/modules/item/item.types.ts +0 -74
  82. package/modules/job/index.ts +0 -5
  83. package/modules/job/job.models.ts +0 -14
  84. package/modules/job/job.router.ts +0 -44
  85. package/modules/job/job.schema.ts +0 -9
  86. package/modules/job/job.service.ts +0 -243
  87. package/modules/job/job.types.ts +0 -23
  88. package/modules/market/index.ts +0 -5
  89. package/modules/market/market.models.ts +0 -113
  90. package/modules/market/market.router.ts +0 -73
  91. package/modules/market/market.schema.ts +0 -140
  92. package/modules/market/market.service.ts +0 -122
  93. package/modules/market/market.types.ts +0 -56
  94. package/modules/product/index.ts +0 -5
  95. package/modules/product/product.models.ts +0 -166
  96. package/modules/product/product.router.ts +0 -93
  97. package/modules/product/product.schema.ts +0 -149
  98. package/modules/product/product.service.ts +0 -160
  99. package/modules/product/product.types.ts +0 -33
  100. package/modules/profile/index.ts +0 -5
  101. package/modules/profile/profile.models.ts +0 -214
  102. package/modules/profile/profile.router.ts +0 -72
  103. package/modules/profile/profile.schema.ts +0 -156
  104. package/modules/profile/profile.service.ts +0 -147
  105. package/modules/profile/profile.types.ts +0 -22
  106. package/modules/raffle/index.ts +0 -5
  107. package/modules/raffle/raffle.models.ts +0 -44
  108. package/modules/raffle/raffle.router.ts +0 -90
  109. package/modules/raffle/raffle.schema.ts +0 -32
  110. package/modules/raffle/raffle.service.ts +0 -167
  111. package/modules/raffle/raffle.types.ts +0 -30
  112. package/modules/skill/index.ts +0 -5
  113. package/modules/skill/skill.models.ts +0 -16
  114. package/modules/skill/skill.router.ts +0 -201
  115. package/modules/skill/skill.schema.ts +0 -40
  116. package/modules/skill/skill.service.ts +0 -390
  117. package/modules/skill/skill.types.ts +0 -33
  118. package/modules/video/index.ts +0 -5
  119. package/modules/video/video.models.ts +0 -25
  120. package/modules/video/video.router.ts +0 -143
  121. package/modules/video/video.schema.ts +0 -46
  122. package/modules/video/video.service.ts +0 -274
  123. package/modules/video/video.types.ts +0 -33
  124. package/util/db/index.ts +0 -7
  125. package/util/db/isPostgresError.ts +0 -9
  126. package/util/db/isUniqueConstraintViolation.ts +0 -3
  127. package/util/db.ts +0 -62
  128. package/util/index.ts +0 -351
  129. /package/{util/api.ts → api.ts} +0 -0
  130. /package/{util/array.ts → array.ts} +0 -0
  131. /package/{util/browser.ts → browser.ts} +0 -0
  132. /package/{util/codebase.ts → codebase.ts} +0 -0
  133. /package/{util/config.ts → config.ts} +0 -0
  134. /package/{util/decoder.test.ts → decoder.test.ts} +0 -0
  135. /package/{util/decoder.ts → decoder.ts} +0 -0
  136. /package/{util/format.ts → format.ts} +0 -0
  137. /package/{util/guid.ts → guid.ts} +0 -0
  138. /package/{util/json.ts → json.ts} +0 -0
  139. /package/{util/log.ts → log.ts} +0 -0
  140. /package/{util/math.ts → math.ts} +0 -0
  141. /package/{util/merkle.ts → merkle.ts} +0 -0
  142. /package/{util/number.ts → number.ts} +0 -0
  143. /package/{util/object.ts → object.ts} +0 -0
  144. /package/{util/otp.ts → otp.ts} +0 -0
  145. /package/{util/physics.ts → physics.ts} +0 -0
  146. /package/{util/process.ts → process.ts} +0 -0
  147. /package/{util/rpc.ts → rpc.ts} +0 -0
  148. /package/{util/seer.ts → seer.ts} +0 -0
  149. /package/{util/string.ts → string.ts} +0 -0
  150. /package/{util/text.ts → text.ts} +0 -0
  151. /package/{util/time → time}/date.ts +0 -0
  152. /package/{util/time → time}/fancyTimeFormat.ts +0 -0
  153. /package/{util/time → time}/index.ts +0 -0
  154. /package/{util/time → time}/now.ts +0 -0
  155. /package/{util/types → types}/mongo.d.ts +0 -0
  156. /package/{util/web3 → web3}/httpProvider.ts +0 -0
  157. /package/{util/web3.ts → web3.ts} +0 -0
  158. /package/{util/websocket.ts → websocket.ts} +0 -0
  159. /package/{util/zk.ts → zk.ts} +0 -0
  160. /package/{util/zod.ts → zod.ts} +0 -0
@@ -1,568 +0,0 @@
1
- // node/modules/core/mail/applyPatchesOrMail.ts
2
- import get from 'lodash/get';
3
- import set from 'lodash/set';
4
- import type { RouterContext } from '../core.types';
5
- import type { PatchOp, EntityPatch } from '../../../types';
6
-
7
- // Reuse your existing function or move it here
8
- export function applyPatchToObject(obj: any, patch: PatchOp[]) {
9
- for (const p of patch) {
10
- if (p.op === 'set') {
11
- set(obj, p.key, p.value);
12
- } else if (p.op === 'unset') {
13
- const parts = p.key.split('.');
14
- const last = parts.pop();
15
- const parent = parts.reduce((acc: any, k) => (acc ? acc[k] : undefined), obj);
16
- if (parent && last) delete parent[last];
17
- } else if (p.op === 'inc') {
18
- const cur = Number(get(obj, p.key)) || 0;
19
- set(obj, p.key, cur + Number(p.value || 0));
20
- } else if (p.op === 'push') {
21
- const cur = get(obj, p.key);
22
- const arr = Array.isArray(cur) ? cur : [];
23
- arr.push(p.value);
24
- set(obj, p.key, arr);
25
- } else if (p.op === 'merge') {
26
- const cur = get(obj, p.key);
27
- const base = cur && typeof cur === 'object' ? cur : {};
28
- set(obj, p.key, { ...base, ...(p.value || {}) });
29
- }
30
- }
31
- }
32
-
33
- // ─────────────────────────────────────────────────────────────
34
- // Inventory normalization hooks
35
- // ─────────────────────────────────────────────────────────────
36
-
37
- async function ensureItemByKey(ctx: RouterContext, itemKey: string, name?: string) {
38
- const Item = (ctx.app as any)?.model?.Item;
39
- if (!Item) return null;
40
-
41
- const found = await Item.findOne?.({ key: itemKey })?.exec?.();
42
- if (found) return found;
43
-
44
- try {
45
- const res = await Item.findOneAndUpdate?.(
46
- { key: itemKey },
47
- {
48
- $setOnInsert: {
49
- key: itemKey,
50
- name: name || itemKey,
51
- status: 'Active',
52
- meta: { name: name || itemKey },
53
- },
54
- },
55
- { new: true, upsert: true }
56
- )?.exec?.();
57
-
58
- return res || (await Item.findOne?.({ key: itemKey })?.exec?.());
59
- } catch {
60
- return null;
61
- }
62
- }
63
-
64
- /**
65
- * Normalizes inventory patch ops:
66
- * push inventory.0.items { itemKey, quantity? } -> repeated push { itemId, ... }
67
- */
68
- export async function normalizeInventoryPatch(ctx: RouterContext, patch: PatchOp[]) {
69
- const out: PatchOp[] = [];
70
-
71
- for (const p of patch) {
72
- if (p.op === 'push' && (p.key === 'inventory.0.items' || p.key.startsWith('inventory.0.items'))) {
73
- const v = (p as any).value || {};
74
-
75
- // already normalized
76
- if (v.itemId) {
77
- const q = Math.max(1, Number(v.quantity ?? 1));
78
- for (let i = 0; i < q; i++) {
79
- out.push({
80
- op: 'push',
81
- key: p.key,
82
- value: { itemId: v.itemId, x: v.x ?? 1, y: v.y ?? 1, meta: v.meta ?? undefined },
83
- });
84
- }
85
- continue;
86
- }
87
-
88
- if (v.itemKey) {
89
- const itemKey = String(v.itemKey);
90
- const item = await ensureItemByKey(ctx, itemKey, itemKey);
91
- const q = Math.max(1, Number(v.quantity ?? 1));
92
-
93
- if (item?._id) {
94
- for (let i = 0; i < q; i++) {
95
- out.push({
96
- op: 'push',
97
- key: p.key,
98
- value: { itemId: item._id, x: v.x ?? 1, y: v.y ?? 1, meta: v.meta ?? undefined },
99
- });
100
- }
101
- } else {
102
- // fallback keep itemKey
103
- for (let i = 0; i < q; i++) {
104
- out.push({
105
- op: 'push',
106
- key: p.key,
107
- value: { itemKey, x: v.x ?? 1, y: v.y ?? 1, meta: v.meta ?? undefined },
108
- });
109
- }
110
- }
111
-
112
- continue;
113
- }
114
- }
115
-
116
- out.push(p);
117
- }
118
-
119
- return out;
120
- }
121
-
122
- // ─────────────────────────────────────────────────────────────
123
- // Generic sync emission (NO wrappers)
124
- // ─────────────────────────────────────────────────────────────
125
-
126
- type InventorySyncOp =
127
- | { op: 'add'; itemKey: string; quantity?: number }
128
- | { op: 'remove'; itemKey: string; quantity?: number };
129
-
130
- function inventoryOpsFromPatchOps(patchOps: any[]): InventorySyncOp[] {
131
- const ops: InventorySyncOp[] = [];
132
- const list = Array.isArray(patchOps) ? patchOps : [];
133
-
134
- for (const op of list) {
135
- const key = String(op?.key || '');
136
- const isInvItems =
137
- key === 'inventory.0.items' ||
138
- key.startsWith('inventory.0.items') ||
139
- // allow future bags without rewriting
140
- key.includes('.items');
141
-
142
- if (!isInvItems) continue;
143
-
144
- if (op?.op === 'push') {
145
- const v = op?.value || {};
146
- const itemKey = v?.itemKey ?? v?.itemId;
147
- if (!itemKey) continue;
148
- const qty = Number(v?.quantity ?? 1);
149
- ops.push({ op: 'add', itemKey: String(itemKey), quantity: Number.isFinite(qty) ? qty : 1 });
150
- continue;
151
- }
152
-
153
- if (op?.op === 'pull') {
154
- const v = op?.value || {};
155
- const itemKey = v?.itemKey ?? v?.itemId;
156
- if (!itemKey) continue;
157
- const qty = Number(v?.quantity ?? 1);
158
- ops.push({ op: 'remove', itemKey: String(itemKey), quantity: Number.isFinite(qty) ? qty : 1 });
159
- continue;
160
- }
161
- }
162
-
163
- return ops;
164
- }
165
-
166
- function inventoryOpsFromEntityPatches(
167
- patches: EntityPatch[] | undefined
168
- ): Array<{ characterId: string; ops: InventorySyncOp[] }> {
169
- const out: Array<{ characterId: string; ops: InventorySyncOp[] }> = [];
170
- for (const ep of patches || []) {
171
- if (ep?.entityType !== 'character.inventory') continue;
172
- const characterId = String(ep?.entityId || '');
173
- if (!characterId) continue;
174
-
175
- const ops = inventoryOpsFromPatchOps((ep as any)?.ops || []);
176
- if (ops.length) out.push({ characterId, ops });
177
- }
178
- return out;
179
- }
180
-
181
- async function emitSyncPatch(ctx: RouterContext, input: { target: string; patch: any; reason?: string }) {
182
- try {
183
- await (ctx.client as any)?.emit?.sync?.mutate?.({
184
- kind: 'patch',
185
- target: input.target,
186
- patch: input.patch,
187
- reason: input.reason,
188
- });
189
- } catch (e) {
190
- // never crash gameplay because push failed
191
- console.warn('[applyPatchesOrMail] emit.sync failed', e);
192
- }
193
- }
194
-
195
- // ─────────────────────────────────────────────────────────────
196
- // Mail + Claim abstraction
197
- // ─────────────────────────────────────────────────────────────
198
-
199
- type MailKind = 'mail' | 'system' | 'support' | 'dm' | 'group';
200
-
201
- export type MailReward = { type: 'item' | 'token' | 'reward'; id: string; quantity?: number; meta?: any };
202
- export type MailEffect = {
203
- type: 'stat' | 'flag' | 'buff' | 'debuff' | 'effect';
204
- key?: string;
205
- delta?: number;
206
- value?: any;
207
- label?: string;
208
- };
209
-
210
- export type MailPatchMessagePayload = {
211
- kind: 'patch-grant';
212
- source: string;
213
- title?: string;
214
- body?: string;
215
-
216
- patches: EntityPatch[];
217
-
218
- ui?: {
219
- rewards?: Array<MailReward>;
220
- effects?: Array<MailEffect>;
221
- };
222
- };
223
-
224
- export type ApplyPatchesResult = {
225
- appliedNow: EntityPatch[];
226
- mailed: {
227
- conversationId: string;
228
- messageId: string;
229
- } | null;
230
- };
231
-
232
- export async function ensureMailConversation(params: {
233
- ctx: RouterContext;
234
- profileId: string;
235
- kind?: MailKind;
236
- conversationKey: string;
237
- title?: string;
238
- category?: string;
239
- importance?: number;
240
- }) {
241
- const { ctx, profileId, kind = 'mail', conversationKey, title, category, importance } = params;
242
-
243
- const mongoose = (ctx.app as any).db?.mongoose ?? (ctx.app as any).mongoose;
244
- const Conversation = (ctx.app as any).model.Conversation;
245
-
246
- const profileObjId = (() => {
247
- try {
248
- return new mongoose.Types.ObjectId(profileId);
249
- } catch {
250
- return profileId;
251
- }
252
- })();
253
-
254
- let convo =
255
- (await Conversation.findOne?.({
256
- kind,
257
- key: conversationKey,
258
- $or: [{ profileId: profileObjId }, { 'participants.profileId': profileObjId }],
259
- status: { $ne: 'Archived' },
260
- })?.exec?.()) ?? null;
261
-
262
- if (convo) return convo;
263
-
264
- const updated =
265
- (await Conversation.findOneAndUpdate?.(
266
- {
267
- kind,
268
- key: conversationKey,
269
- profileId: profileObjId,
270
- status: { $ne: 'Archived' },
271
- },
272
- {
273
- $setOnInsert: {
274
- profileId: profileObjId,
275
- kind,
276
- key: conversationKey,
277
- isLocked: true,
278
- allowUserSend: false,
279
- participants: [{ profileId: profileObjId, role: 'user', unreadCount: 0, lastReadDate: new Date(0) }],
280
- name: title ?? 'System',
281
- category: category ?? 'system',
282
- importance: Number(importance ?? 0),
283
- lastMessageDate: null,
284
- lastMessagePreview: '',
285
- messageCount: 0,
286
- messages: [],
287
- status: 'Active',
288
- },
289
- },
290
- { new: true, upsert: true }
291
- )?.exec?.()) ?? null;
292
-
293
- if (updated) return updated;
294
-
295
- return await Conversation.findOne?.({ kind, conversationKey, profileId: profileObjId })?.exec?.();
296
- }
297
-
298
- function splitClaimable(patches: EntityPatch[]) {
299
- const claimable: EntityPatch[] = [];
300
- const immediate: EntityPatch[] = [];
301
-
302
- for (const p of patches || []) {
303
- if (!p?.entityType || !Array.isArray(p.ops)) continue;
304
- if ((p as any).claimable) claimable.push(p);
305
- else immediate.push(p);
306
- }
307
-
308
- return { claimable, immediate };
309
- }
310
-
311
- /**
312
- * ✅ Applies NON-claimable patches immediately.
313
- * ✅ Mails ALL claimable patches.
314
- * ✅ Emits generic sync.patch for any immediate inventory change.
315
- */
316
- export async function applyPatchesWithInventoryViaMail(params: {
317
- ctx: RouterContext;
318
- profile: any;
319
- character?: any;
320
- patches: EntityPatch[];
321
- mail: {
322
- profileId: string;
323
- kind?: MailKind;
324
- conversationKey: string;
325
- source: string;
326
- title?: string;
327
- body?: string;
328
- category?: string;
329
- importance?: number;
330
- ui?: MailPatchMessagePayload['ui'];
331
- dedupeKey?: string;
332
- };
333
- }): Promise<ApplyPatchesResult> {
334
- const { ctx, profile, character, patches, mail } = params;
335
-
336
- const { claimable, immediate } = splitClaimable(patches);
337
-
338
- // Track inventory changes applied immediately
339
- const immediateInventorySync = inventoryOpsFromEntityPatches(immediate);
340
-
341
- // 1) Apply immediate patches now
342
- for (const patch of immediate) {
343
- if (patch.entityType === 'profile.meta') {
344
- if (!profile.meta) profile.meta = {};
345
- applyPatchToObject(profile.meta, patch.ops);
346
- profile.markModified?.('meta');
347
- } else if (patch.entityType === 'character.data') {
348
- if (!character) continue;
349
- if (!character.data) character.data = {};
350
- applyPatchToObject(character.data, patch.ops);
351
- character.markModified?.('data');
352
- } else if (patch.entityType === 'character.inventory') {
353
- if (!character) continue;
354
- if (!Array.isArray(character.inventory)) character.inventory = [];
355
- if (!character.inventory[0]) character.inventory[0] = { items: [] };
356
-
357
- const normalized = await normalizeInventoryPatch(ctx, patch.ops);
358
- applyPatchToObject(character, normalized);
359
- character.markModified?.('inventory');
360
- } else {
361
- throw new Error(`applyPatchesWithInventoryViaMail: unsupported entityType=${patch.entityType}`);
362
- }
363
- }
364
-
365
- // persist immediate effects
366
- await profile.save?.();
367
- if (character && immediate.some((p) => p.entityType?.startsWith('character.'))) {
368
- await character.save?.();
369
- }
370
-
371
- // ✅ emit sync for immediate inventory changes
372
- for (const hit of immediateInventorySync) {
373
- await emitSyncPatch(ctx, {
374
- target: 'character.inventory',
375
- patch: {
376
- characterId: hit.characterId,
377
- ops: hit.ops, // already reduced, generic
378
- mode: 'patch',
379
- source: mail.source,
380
- reason: 'immediate',
381
- },
382
- reason: 'immediate',
383
- });
384
- }
385
-
386
- // 2) If no claimable patches, done
387
- if (!claimable.length) return { appliedNow: immediate, mailed: null };
388
-
389
- // 3) Find/create inbox + create message with claim payload (deduped)
390
- const convo = await ensureMailConversation({
391
- ctx,
392
- profileId: mail.profileId,
393
- kind: mail.kind ?? 'mail',
394
- conversationKey: mail.conversationKey,
395
- title: mail.title ?? 'System',
396
- category: mail.category ?? 'system',
397
- importance: mail.importance ?? 0,
398
- });
399
-
400
- const ConversationMessage = (ctx.app as any).model.ConversationMessage;
401
- const Conversation = (ctx.app as any).model.Conversation;
402
-
403
- const dedupeKey = mail.dedupeKey ?? null;
404
-
405
- if (dedupeKey) {
406
- const existing = await ConversationMessage.findOne?.({
407
- conversationId: convo._id,
408
- 'claim.dedupeKey': dedupeKey,
409
- })?.exec?.();
410
-
411
- if (existing) {
412
- return {
413
- appliedNow: immediate,
414
- mailed: { conversationId: String(convo._id), messageId: String(existing._id) },
415
- };
416
- }
417
- }
418
-
419
- const payload: MailPatchMessagePayload = {
420
- kind: 'patch-grant',
421
- source: mail.source,
422
- title: mail.title,
423
- body: mail.body,
424
- patches: claimable,
425
- ui: mail.ui,
426
- };
427
-
428
- const msg = await ConversationMessage.create?.({
429
- conversationId: convo._id,
430
- role: 'system',
431
- type: 'reward',
432
- content: mail.body ?? '',
433
- payload,
434
- claim: {
435
- isClaimable: true,
436
- claimedDate: null,
437
- claimedByProfileId: null,
438
- dedupeKey,
439
- attachments: [],
440
- },
441
- });
442
-
443
- try {
444
- const preview = (mail.title ? `${mail.title} — ` : '') + (mail.body ?? '');
445
- await Conversation.updateOne?.(
446
- { _id: convo._id },
447
- {
448
- $set: {
449
- lastMessageDate: new Date(),
450
- lastMessagePreview: String(preview).slice(0, 140),
451
- },
452
- $inc: { messageCount: 1 },
453
- $push: { messages: msg._id },
454
- }
455
- )?.exec?.();
456
- } catch {
457
- // ignore
458
- }
459
-
460
- return { appliedNow: immediate, mailed: { conversationId: String(convo._id), messageId: String(msg._id) } };
461
- }
462
-
463
- /**
464
- * ✅ Central claim handler
465
- * ✅ Emits generic sync.patch for any claimed inventory changes.
466
- */
467
- export async function claimMailMessage(params: {
468
- ctx: RouterContext;
469
- profile: any;
470
- character?: any;
471
- messageId: string;
472
- }): Promise<{ ok: true }> {
473
- const { ctx, profile, character, messageId } = params;
474
-
475
- const ConversationMessage = (ctx.app as any).model.ConversationMessage;
476
-
477
- const msg = await ConversationMessage.findOneAndUpdate?.(
478
- {
479
- _id: messageId,
480
- 'claim.isClaimable': true,
481
- 'claim.claimedDate': null,
482
- 'claim.claimedByProfileId': null,
483
- },
484
- {
485
- $set: {
486
- 'claim.claimedDate': new Date(),
487
- 'claim.claimedByProfileId': String(profile._id),
488
- },
489
- },
490
- { new: true }
491
- )?.exec?.();
492
-
493
- if (!msg) {
494
- const err: any = new Error('Already claimed or not claimable');
495
- err.code = 'ALREADY_CLAIMED';
496
- throw err;
497
- }
498
-
499
- const payload = msg.payload as MailPatchMessagePayload | undefined;
500
- if (!payload || payload.kind !== 'patch-grant' || !Array.isArray(payload.patches)) return { ok: true };
501
-
502
- // collect inventory sync intents BEFORE normalization mutates anything
503
- const claimedInventorySync = inventoryOpsFromEntityPatches(payload.patches);
504
-
505
- let touchedProfileMeta = false;
506
- let touchedCharacterData = false;
507
- let touchedCharacterInventory = false;
508
-
509
- for (const patch of payload.patches) {
510
- if (!patch?.entityType || !Array.isArray(patch.ops)) continue;
511
-
512
- if (patch.entityType === 'profile.meta') {
513
- if (!profile.meta) profile.meta = {};
514
- applyPatchToObject(profile.meta, patch.ops);
515
- touchedProfileMeta = true;
516
- continue;
517
- }
518
-
519
- if (patch.entityType === 'character.data') {
520
- if (!character) throw new Error('No character loaded for character.data claim');
521
- if (!character.data) character.data = {};
522
- applyPatchToObject(character.data, patch.ops);
523
- touchedCharacterData = true;
524
- continue;
525
- }
526
-
527
- if (patch.entityType === 'character.inventory') {
528
- if (!character) throw new Error('No character loaded for inventory claim');
529
-
530
- if (!Array.isArray(character.inventory)) character.inventory = [];
531
- if (!character.inventory[0]) character.inventory[0] = { items: [] };
532
-
533
- const normalized = await normalizeInventoryPatch(ctx, patch.ops);
534
- applyPatchToObject(character, normalized);
535
- touchedCharacterInventory = true;
536
- continue;
537
- }
538
-
539
- throw new Error(`claimMailMessage: unsupported entityType=${patch.entityType}`);
540
- }
541
-
542
- if (touchedProfileMeta) profile.markModified?.('meta');
543
- if (touchedCharacterData) character?.markModified?.('data');
544
- if (touchedCharacterInventory) character?.markModified?.('inventory');
545
-
546
- await profile.save?.();
547
- if (character && (touchedCharacterData || touchedCharacterInventory)) {
548
- await character.save?.();
549
- }
550
-
551
- // ✅ emit sync for claimed inventory changes
552
- for (const hit of claimedInventorySync) {
553
- await ctx.client.emit.sync.mutate({
554
- kind: 'patch',
555
- target: 'character.inventory',
556
- patch: {
557
- characterId: hit.characterId,
558
- ops: hit.ops,
559
- mode: 'patch',
560
- source: payload.source || 'mail.claim',
561
- reason: 'claim',
562
- },
563
- reason: 'claim',
564
- });
565
- }
566
-
567
- return { ok: true };
568
- }