pixelia_model.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. const pull = require('../server/node_modules/pull-stream');
  2. module.exports = ({ cooler }) => {
  3. let ssb;
  4. const openSsb = async () => {
  5. if (!ssb) ssb = await cooler.open();
  6. return ssb;
  7. };
  8. const getPixelByCoordinate = async (coordinateKey) => {
  9. const ssbClient = await openSsb();
  10. const messages = await new Promise((res, rej) => {
  11. pull(
  12. ssbClient.createLogStream(),
  13. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  14. );
  15. });
  16. const tombstoned = new Set(
  17. messages
  18. .filter(m => m.value?.content?.type === 'tombstone' && m.value?.content?.target)
  19. .map(m => m.value.content.target)
  20. );
  21. const replaces = new Map();
  22. const byId = new Map();
  23. for (const m of messages) {
  24. const c = m.value?.content;
  25. const k = m.key;
  26. if (!c || c.type !== 'pixelia' || c.coordinateKey !== coordinateKey) continue;
  27. if (tombstoned.has(k)) continue;
  28. if (c.replaces) replaces.set(c.replaces, k);
  29. byId.set(k, m);
  30. }
  31. for (const r of replaces.keys()) {
  32. byId.delete(r);
  33. }
  34. return [...byId.values()][0] || null;
  35. };
  36. const paintPixel = async (x, y, color) => {
  37. if (x < 1 || x > 50 || y < 1 || y > 200) {
  38. throw new Error('Coordinates out of bounds. Please use x (1-50) and y (1-200)');
  39. }
  40. const ssbClient = await openSsb();
  41. const userId = ssbClient.id;
  42. const coordinateKey = `${x}:${y}`;
  43. const existingPixel = await getPixelByCoordinate(coordinateKey);
  44. if (existingPixel) {
  45. const tombstone = {
  46. type: 'tombstone',
  47. target: existingPixel.key,
  48. deletedAt: new Date().toISOString()
  49. };
  50. await new Promise((resolve, reject) =>
  51. ssbClient.publish(tombstone, err => err ? reject(err) : resolve())
  52. );
  53. }
  54. const contributors = existingPixel?.value?.content?.contributors_inhabitants || [];
  55. const contributors_inhabitants = contributors.includes(userId)
  56. ? contributors
  57. : [...contributors, userId];
  58. const content = {
  59. type: 'pixelia',
  60. x: x - 1,
  61. y: y - 1,
  62. color,
  63. author: userId,
  64. contributors_inhabitants,
  65. timestamp: Date.now(),
  66. coordinateKey,
  67. replaces: existingPixel?.key || null
  68. };
  69. await new Promise((resolve, reject) => {
  70. ssbClient.publish(content, (err) => err ? reject(err) : resolve());
  71. });
  72. };
  73. const listPixels = async () => {
  74. const ssbClient = await openSsb();
  75. const messages = await new Promise((res, rej) => {
  76. pull(
  77. ssbClient.createLogStream(),
  78. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  79. );
  80. });
  81. const tombstoned = new Set();
  82. const replaces = new Map();
  83. const byKey = new Map();
  84. for (const m of messages) {
  85. const c = m.value?.content;
  86. const k = m.key;
  87. if (!c) continue;
  88. if (c.type === 'tombstone' && c.target) {
  89. tombstoned.add(c.target);
  90. continue;
  91. }
  92. if (c.type === 'pixelia') {
  93. if (tombstoned.has(k)) continue;
  94. if (c.replaces) replaces.set(c.replaces, k);
  95. byKey.set(k, m);
  96. }
  97. }
  98. for (const replaced of replaces.keys()) {
  99. byKey.delete(replaced);
  100. }
  101. return Array.from(byKey.values()).map(m => ({
  102. x: m.value.content.x + 1,
  103. y: m.value.content.y + 1,
  104. color: m.value.content.color,
  105. author: m.value.content.author,
  106. contributors_inhabitants: m.value.content.contributors_inhabitants || [],
  107. timestamp: m.value.timestamp
  108. }));
  109. };
  110. return {
  111. paintPixel,
  112. listPixels
  113. };
  114. };