Oasis 0.8.7 mobile (nodejs-project baseline)

This commit is contained in:
s1to 2026-07-09 09:56:30 +02:00
commit e563b86ede
226 changed files with 141968 additions and 0 deletions

144
src/models/pixelia_model.js Normal file
View file

@ -0,0 +1,144 @@
const pull = require('../server/node_modules/pull-stream');
const { getConfig } = require('../configs/config-manager.js');
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
const logLimit = getConfig().ssbLogStream?.limit || 1000;
module.exports = ({ cooler }) => {
let ssb;
const openSsb = async () => {
if (!ssb) ssb = await cooler.open();
return ssb;
};
const getPixelByCoordinate = async (coordinateKey) => {
const ssbClient = await openSsb();
const messages = await new Promise((res, rej) => {
pull(
ssbClient.createLogStream({ limit: logLimit }),
pull.collect((err, msgs) => err ? rej(err) : res(msgs))
);
});
const tombstoned = buildValidatedTombstoneSet(messages);
const replaces = new Map();
const byId = new Map();
for (const m of messages) {
const c = m.value?.content;
const k = m.key;
if (!c || c.type !== 'pixelia' || c.coordinateKey !== coordinateKey) continue;
if (tombstoned.has(k)) continue;
if (c.replaces) replaces.set(c.replaces, k);
byId.set(k, m);
}
for (const [replacedId, replacingId] of replaces.entries()) {
const orig = byId.get(replacedId);
if (!orig) continue;
const rep = byId.get(replacingId);
if (!rep || rep.value.author !== orig.value.author) {
byId.delete(replacingId);
continue;
}
byId.delete(replacedId);
}
return [...byId.values()][0] || null;
};
const paintPixel = async (x, y, color) => {
if (x < 1 || x > 50 || y < 1 || y > 200) {
throw new Error('Coordinates out of bounds. Please use x (1-50) and y (1-200)');
}
const ssbClient = await openSsb();
const userId = ssbClient.id;
const coordinateKey = `${x}:${y}`;
const existingPixel = await getPixelByCoordinate(coordinateKey);
if (existingPixel) {
const tombstone = {
type: 'tombstone',
target: existingPixel.key,
deletedAt: new Date().toISOString()
};
await new Promise((resolve, reject) =>
ssbClient.publish(tombstone, err => err ? reject(err) : resolve())
);
}
const contributors = existingPixel?.value?.content?.contributors_inhabitants || [];
const contributors_inhabitants = contributors.includes(userId)
? contributors
: [...contributors, userId];
const content = {
type: 'pixelia',
x: x - 1,
y: y - 1,
color,
author: userId,
contributors_inhabitants,
timestamp: Date.now(),
coordinateKey,
replaces: existingPixel?.key || null
};
await new Promise((resolve, reject) => {
ssbClient.publish(content, (err) => err ? reject(err) : resolve());
});
};
const listPixels = async () => {
const ssbClient = await openSsb();
const messages = await new Promise((res, rej) => {
pull(
ssbClient.createLogStream({ limit: logLimit }),
pull.collect((err, msgs) => err ? rej(err) : res(msgs))
);
});
const tombstoned = buildValidatedTombstoneSet(messages);
const replaces = new Map();
const byKey = new Map();
for (const m of messages) {
const c = m.value?.content;
const k = m.key;
if (!c) continue;
if (c.type === 'tombstone') continue;
if (c.type === 'pixelia') {
if (tombstoned.has(k)) continue;
if (c.replaces) replaces.set(c.replaces, k);
byKey.set(k, m);
}
}
for (const [replacedId, replacingId] of replaces.entries()) {
const orig = byKey.get(replacedId);
if (!orig) continue;
const rep = byKey.get(replacingId);
if (!rep || rep.value.author !== orig.value.author) {
byKey.delete(replacingId);
continue;
}
byKey.delete(replacedId);
}
return Array.from(byKey.values()).map(m => ({
x: m.value.content.x + 1,
y: m.value.content.y + 1,
color: m.value.content.color,
author: m.value.author,
contributors_inhabitants: m.value.content.contributors_inhabitants || [],
timestamp: m.value.timestamp
}));
};
return {
paintPixel,
listPixels
};
};