@arken/node 1.5.1 → 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 (148) hide show
  1. package/db.ts +76 -1
  2. package/index.ts +351 -18
  3. package/package.json +3 -3
  4. package/tsconfig.json +33 -2
  5. package/util.ts +1 -0
  6. package/modules/area/area.models.ts +0 -15
  7. package/modules/area/area.router.ts +0 -74
  8. package/modules/area/area.schema.ts +0 -22
  9. package/modules/area/area.service.ts +0 -124
  10. package/modules/area/area.types.ts +0 -26
  11. package/modules/area/index.ts +0 -5
  12. package/modules/asset/asset.models.ts +0 -59
  13. package/modules/asset/asset.router.ts +0 -55
  14. package/modules/asset/asset.schema.ts +0 -27
  15. package/modules/asset/asset.service.ts +0 -85
  16. package/modules/asset/asset.types.ts +0 -22
  17. package/modules/asset/index.ts +0 -5
  18. package/modules/chain/chain.models.ts +0 -50
  19. package/modules/chain/chain.router.ts +0 -104
  20. package/modules/chain/chain.schema.ts +0 -52
  21. package/modules/chain/chain.service.ts +0 -167
  22. package/modules/chain/chain.types.ts +0 -24
  23. package/modules/chain/index.ts +0 -5
  24. package/modules/character/character.models.ts +0 -174
  25. package/modules/character/character.router.ts +0 -314
  26. package/modules/character/character.schema.ts +0 -147
  27. package/modules/character/character.service.ts +0 -876
  28. package/modules/character/character.types.ts +0 -64
  29. package/modules/character/index.ts +0 -5
  30. package/modules/chat/chat.models.ts +0 -43
  31. package/modules/chat/chat.router.ts +0 -67
  32. package/modules/chat/chat.schema.ts +0 -36
  33. package/modules/chat/chat.service.ts +0 -128
  34. package/modules/chat/chat.types.ts +0 -20
  35. package/modules/chat/index.ts +0 -5
  36. package/modules/collection/collection.models.ts +0 -76
  37. package/modules/collection/collection.router.ts +0 -91
  38. package/modules/collection/collection.schema.ts +0 -90
  39. package/modules/collection/collection.service.ts +0 -192
  40. package/modules/collection/collection.types.ts +0 -36
  41. package/modules/collection/index.ts +0 -5
  42. package/modules/core/core.models.ts +0 -1380
  43. package/modules/core/core.router.ts +0 -1781
  44. package/modules/core/core.schema.ts +0 -847
  45. package/modules/core/core.service.ts +0 -2824
  46. package/modules/core/core.types.ts +0 -340
  47. package/modules/core/index.ts +0 -5
  48. package/modules/core/mail/applyPatchesOrMail.ts +0 -568
  49. package/modules/core/mail/mailClaimablePatchesBatch.ts +0 -381
  50. package/modules/game/game.models.ts +0 -53
  51. package/modules/game/game.router.ts +0 -110
  52. package/modules/game/game.schema.ts +0 -23
  53. package/modules/game/game.service.ts +0 -143
  54. package/modules/game/game.types.ts +0 -28
  55. package/modules/game/index.ts +0 -5
  56. package/modules/interface/index.ts +0 -5
  57. package/modules/interface/interface.canonicalize.ts +0 -279
  58. package/modules/interface/interface.models.ts +0 -40
  59. package/modules/interface/interface.router.ts +0 -175
  60. package/modules/interface/interface.schema.ts +0 -59
  61. package/modules/interface/interface.service.ts +0 -356
  62. package/modules/interface/interface.types.ts +0 -25
  63. package/modules/item/index.ts +0 -5
  64. package/modules/item/item.models.ts +0 -124
  65. package/modules/item/item.router.ts +0 -103
  66. package/modules/item/item.schema.ts +0 -120
  67. package/modules/item/item.service.ts +0 -167
  68. package/modules/item/item.types.ts +0 -74
  69. package/modules/job/index.ts +0 -5
  70. package/modules/job/job.models.ts +0 -14
  71. package/modules/job/job.router.ts +0 -44
  72. package/modules/job/job.schema.ts +0 -9
  73. package/modules/job/job.service.ts +0 -243
  74. package/modules/job/job.types.ts +0 -23
  75. package/modules/market/index.ts +0 -5
  76. package/modules/market/market.models.ts +0 -113
  77. package/modules/market/market.router.ts +0 -73
  78. package/modules/market/market.schema.ts +0 -140
  79. package/modules/market/market.service.ts +0 -122
  80. package/modules/market/market.types.ts +0 -56
  81. package/modules/product/index.ts +0 -5
  82. package/modules/product/product.models.ts +0 -166
  83. package/modules/product/product.router.ts +0 -93
  84. package/modules/product/product.schema.ts +0 -149
  85. package/modules/product/product.service.ts +0 -160
  86. package/modules/product/product.types.ts +0 -33
  87. package/modules/profile/index.ts +0 -5
  88. package/modules/profile/profile.models.ts +0 -214
  89. package/modules/profile/profile.router.ts +0 -72
  90. package/modules/profile/profile.schema.ts +0 -156
  91. package/modules/profile/profile.service.ts +0 -149
  92. package/modules/profile/profile.types.ts +0 -22
  93. package/modules/raffle/index.ts +0 -5
  94. package/modules/raffle/raffle.models.ts +0 -44
  95. package/modules/raffle/raffle.router.ts +0 -90
  96. package/modules/raffle/raffle.schema.ts +0 -32
  97. package/modules/raffle/raffle.service.ts +0 -167
  98. package/modules/raffle/raffle.types.ts +0 -30
  99. package/modules/skill/index.ts +0 -5
  100. package/modules/skill/skill.models.ts +0 -16
  101. package/modules/skill/skill.router.ts +0 -201
  102. package/modules/skill/skill.schema.ts +0 -40
  103. package/modules/skill/skill.service.ts +0 -390
  104. package/modules/skill/skill.types.ts +0 -33
  105. package/modules/video/index.ts +0 -5
  106. package/modules/video/video.models.ts +0 -25
  107. package/modules/video/video.router.ts +0 -143
  108. package/modules/video/video.schema.ts +0 -46
  109. package/modules/video/video.service.ts +0 -274
  110. package/modules/video/video.types.ts +0 -33
  111. package/util/db/index.ts +0 -7
  112. package/util/db/isPostgresError.ts +0 -9
  113. package/util/db/isUniqueConstraintViolation.ts +0 -3
  114. package/util/db.ts +0 -62
  115. package/util/index.ts +0 -351
  116. /package/{util/api.ts → api.ts} +0 -0
  117. /package/{util/array.ts → array.ts} +0 -0
  118. /package/{util/browser.ts → browser.ts} +0 -0
  119. /package/{util/codebase.ts → codebase.ts} +0 -0
  120. /package/{util/config.ts → config.ts} +0 -0
  121. /package/{util/decoder.test.ts → decoder.test.ts} +0 -0
  122. /package/{util/decoder.ts → decoder.ts} +0 -0
  123. /package/{util/format.ts → format.ts} +0 -0
  124. /package/{util/guid.ts → guid.ts} +0 -0
  125. /package/{util/json.ts → json.ts} +0 -0
  126. /package/{util/log.ts → log.ts} +0 -0
  127. /package/{util/math.ts → math.ts} +0 -0
  128. /package/{util/merkle.ts → merkle.ts} +0 -0
  129. /package/{util/mongo.ts → mongo.ts} +0 -0
  130. /package/{util/number.ts → number.ts} +0 -0
  131. /package/{util/object.ts → object.ts} +0 -0
  132. /package/{util/otp.ts → otp.ts} +0 -0
  133. /package/{util/physics.ts → physics.ts} +0 -0
  134. /package/{util/process.ts → process.ts} +0 -0
  135. /package/{util/rpc.ts → rpc.ts} +0 -0
  136. /package/{util/seer.ts → seer.ts} +0 -0
  137. /package/{util/string.ts → string.ts} +0 -0
  138. /package/{util/text.ts → text.ts} +0 -0
  139. /package/{util/time → time}/date.ts +0 -0
  140. /package/{util/time → time}/fancyTimeFormat.ts +0 -0
  141. /package/{util/time → time}/index.ts +0 -0
  142. /package/{util/time → time}/now.ts +0 -0
  143. /package/{util/types → types}/mongo.d.ts +0 -0
  144. /package/{util/web3 → web3}/httpProvider.ts +0 -0
  145. /package/{util/web3.ts → web3.ts} +0 -0
  146. /package/{util/websocket.ts → websocket.ts} +0 -0
  147. /package/{util/zk.ts → zk.ts} +0 -0
  148. /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
- }