blockchain_model.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. const pull = require('../server/node_modules/pull-stream');
  2. const { config } = require('../server/SSB_server.js');
  3. const { getConfig } = require('../configs/config-manager.js');
  4. const logLimit = getConfig().ssbLogStream?.limit || 1000;
  5. module.exports = ({ cooler }) => {
  6. let ssb;
  7. const openSsb = async () => {
  8. if (!ssb) ssb = await cooler.open();
  9. return ssb;
  10. };
  11. const hasBlob = async (ssbClient, url) =>
  12. new Promise(resolve => ssbClient.blobs.has(url, (err, has) => resolve(!err && has)));
  13. return {
  14. async listBlockchain(filter = 'all') {
  15. const ssbClient = await openSsb();
  16. const results = await new Promise((resolve, reject) =>
  17. pull(
  18. ssbClient.createLogStream({ reverse: true, limit: logLimit }),
  19. pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))
  20. )
  21. );
  22. const tombstoned = new Set();
  23. const idToBlock = new Map();
  24. const referencedAsReplaces = new Set();
  25. for (const msg of results) {
  26. const k = msg.key;
  27. const c = msg.value?.content;
  28. const author = msg.value?.author;
  29. if (!c?.type) continue;
  30. if (c.type === 'tombstone' && c.target) {
  31. tombstoned.add(c.target);
  32. idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c });
  33. continue;
  34. }
  35. if (c.replaces) referencedAsReplaces.add(c.replaces);
  36. idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c });
  37. }
  38. const tipBlocks = [];
  39. for (const [id, block] of idToBlock.entries()) {
  40. if (!referencedAsReplaces.has(id) && block.content.replaces) tipBlocks.push(block);
  41. }
  42. for (const [id, block] of idToBlock.entries()) {
  43. if (!block.content.replaces && !referencedAsReplaces.has(id)) tipBlocks.push(block);
  44. }
  45. const groups = {};
  46. for (const block of tipBlocks) {
  47. const ancestor = block.content.replaces || block.id;
  48. if (!groups[ancestor]) groups[ancestor] = [];
  49. groups[ancestor].push(block);
  50. }
  51. const liveTipIds = new Set();
  52. for (const groupBlocks of Object.values(groups)) {
  53. let best = groupBlocks[0];
  54. for (const block of groupBlocks) {
  55. if (block.type === 'market') {
  56. const isClosedSold = s => s === 'SOLD' || s === 'CLOSED';
  57. if (isClosedSold(block.content.status) && !isClosedSold(best.content.status)) {
  58. best = block;
  59. } else if ((block.content.status === best.content.status) && block.ts > best.ts) {
  60. best = block;
  61. }
  62. } else if (block.type === 'job' || block.type === 'forum') {
  63. if (block.ts > best.ts) best = block;
  64. } else {
  65. if (block.ts > best.ts) best = block;
  66. }
  67. }
  68. liveTipIds.add(best.id);
  69. }
  70. const blockData = Array.from(idToBlock.values()).map(block => {
  71. const c = block.content;
  72. const rootDeleted = c?.type === 'forum' && c.root && tombstoned.has(c.root);
  73. return {
  74. ...block,
  75. isTombstoned: tombstoned.has(block.id),
  76. isReplaced: c.replaces
  77. ? (!liveTipIds.has(block.id) || tombstoned.has(block.id))
  78. : referencedAsReplaces.has(block.id) || tombstoned.has(block.id) || rootDeleted
  79. };
  80. });
  81. let filtered = blockData;
  82. if (filter === 'RECENT') {
  83. const now = Date.now();
  84. filtered = blockData.filter(b => b && now - b.ts <= 24 * 60 * 60 * 1000);
  85. }
  86. if (filter === 'MINE') {
  87. filtered = blockData.filter(b => b && b.author === config.keys.id);
  88. }
  89. return filtered.filter(Boolean);
  90. },
  91. async getBlockById(id) {
  92. const ssbClient = await openSsb();
  93. const results = await new Promise((resolve, reject) =>
  94. pull(
  95. ssbClient.createLogStream({ reverse: true, limit: logLimit }),
  96. pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))
  97. )
  98. );
  99. const tombstoned = new Set();
  100. const idToBlock = new Map();
  101. const referencedAsReplaces = new Set();
  102. for (const msg of results) {
  103. const k = msg.key;
  104. const c = msg.value?.content;
  105. const author = msg.value?.author;
  106. if (!c?.type) continue;
  107. if (c.type === 'tombstone' && c.target) {
  108. tombstoned.add(c.target);
  109. idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c });
  110. continue;
  111. }
  112. if (c.replaces) referencedAsReplaces.add(c.replaces);
  113. idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c });
  114. }
  115. const tipBlocks = [];
  116. for (const [bid, block] of idToBlock.entries()) {
  117. if (!referencedAsReplaces.has(bid) && block.content.replaces) tipBlocks.push(block);
  118. }
  119. for (const [bid, block] of idToBlock.entries()) {
  120. if (!block.content.replaces && !referencedAsReplaces.has(bid)) tipBlocks.push(block);
  121. }
  122. const groups = {};
  123. for (const block of tipBlocks) {
  124. const ancestor = block.content.replaces || block.id;
  125. if (!groups[ancestor]) groups[ancestor] = [];
  126. groups[ancestor].push(block);
  127. }
  128. const liveTipIds = new Set();
  129. for (const groupBlocks of Object.values(groups)) {
  130. let best = groupBlocks[0];
  131. for (const block of groupBlocks) {
  132. if (block.type === 'market') {
  133. const isClosedSold = s => s === 'SOLD' || s === 'CLOSED';
  134. if (isClosedSold(block.content.status) && !isClosedSold(best.content.status)) {
  135. best = block;
  136. } else if ((block.content.status === best.content.status) && block.ts > best.ts) {
  137. best = block;
  138. }
  139. } else if (block.type === 'job' || block.type === 'forum') {
  140. if (block.ts > best.ts) best = block;
  141. } else {
  142. if (block.ts > best.ts) best = block;
  143. }
  144. }
  145. liveTipIds.add(best.id);
  146. }
  147. const block = idToBlock.get(id);
  148. if (!block) return null;
  149. if (block.type === 'document') {
  150. const valid = await hasBlob(ssbClient, block.content.url);
  151. if (!valid) return null;
  152. }
  153. const c = block.content;
  154. const rootDeleted = c?.type === 'forum' && c.root && tombstoned.has(c.root);
  155. const isTombstoned = tombstoned.has(block.id);
  156. const isReplaced = c.replaces
  157. ? (!liveTipIds.has(block.id) || tombstoned.has(block.id))
  158. : referencedAsReplaces.has(block.id) || tombstoned.has(block.id) || rootDeleted;
  159. return {
  160. ...block,
  161. isTombstoned,
  162. isReplaced
  163. };
  164. }
  165. };
  166. };