fedipod-bb 0.4.0 → 0.4.1
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.
- package/package.json +1 -1
- package/src/gateway.mjs +74 -16
- package/src/keep.mjs +13 -5
- package/src/setup.mjs +8 -5
package/package.json
CHANGED
package/src/gateway.mjs
CHANGED
|
@@ -27,7 +27,9 @@ export const isForumRow = (rec) => !!rec?.inboxUrl && (rec.kind === 'group' || r
|
|
|
27
27
|
export async function forumFor(ctx, handle, rec) {
|
|
28
28
|
const forumHandle = rec.kind === 'application' ? handle : rec.forum;
|
|
29
29
|
if (!forumHandle) return { skipped: 'the category names no forum; run `fedipod-bb keep` again' };
|
|
30
|
-
|
|
30
|
+
// Read fresh where the front offers it: the forum's row is switched on
|
|
31
|
+
// last, and its first run starts in the same breath.
|
|
32
|
+
const forumRec = forumHandle === handle ? rec : await (ctx.lookupFresh || ctx.lookup)(forumHandle);
|
|
31
33
|
if (!forumRec || !isForumRow(forumRec) || !forumRec.keeper) return { skipped: `@${forumHandle} is not a forum this gateway keeps` };
|
|
32
34
|
if (!forumRec.webId || !forumRec.podHome) return { skipped: 'the forum row names no owner or pod' };
|
|
33
35
|
const home = forumRec.podHome.replace(/\/?$/u, '/');
|
|
@@ -89,7 +91,7 @@ async function readConfig(ctx, f, podFetch) {
|
|
|
89
91
|
export async function ensureForumCopies(ctx, f, config, log) {
|
|
90
92
|
const rows = [[f.forumHandle, f.forumRec]];
|
|
91
93
|
for (const c of config?.categories || []) {
|
|
92
|
-
const r = await ctx.lookup(c.slug);
|
|
94
|
+
const r = await (ctx.lookupFresh || ctx.lookup)(c.slug);
|
|
93
95
|
if (r && isForumRow(r)) rows.push([c.slug, r]);
|
|
94
96
|
}
|
|
95
97
|
const have = new Set();
|
|
@@ -139,6 +141,33 @@ function nextRun(forum, asksPending) {
|
|
|
139
141
|
* Returns what the keeper's bookkeeping expects: { drained, waiting, nextAt }
|
|
140
142
|
* or { skipped, retry }.
|
|
141
143
|
*/
|
|
144
|
+
// A queued delivery's identity: where it goes and what it carries.
|
|
145
|
+
const queueKey = (item) => `${item.inbox}\u0000${typeof item.activity === 'string' ? item.activity : JSON.stringify(item.activity)}`;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The queue after a carry that ran with nobody holding the forum: `before`
|
|
149
|
+
* is what the run took out to carry, `after` what the carry left of it
|
|
150
|
+
* (delivered items gone, failed ones with their next time), `fresh` the
|
|
151
|
+
* queue as the copy holds it now, which the door may have added to in the
|
|
152
|
+
* meantime. A door only ever appends, so: an item the run took out is kept
|
|
153
|
+
* as the carry left it or dropped if delivered; anything else is the door's
|
|
154
|
+
* and stays.
|
|
155
|
+
*/
|
|
156
|
+
export function mergeQueues(before, after, fresh) {
|
|
157
|
+
const taken = new Set(before.map(queueKey));
|
|
158
|
+
const left = new Map(after.map((i) => [queueKey(i), i]));
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const item of fresh) {
|
|
161
|
+
const k = queueKey(item);
|
|
162
|
+
if (!taken.has(k)) { out.push(item); continue; }
|
|
163
|
+
if (left.has(k)) out.push(left.get(k));
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const storesOf = (forum) => [forum.store, ...forum.categories.map((c) => c.store)];
|
|
169
|
+
const deliverersOf = (forum) => [...forum.categories.map((c) => c.deliverer), forum.siteAgent.deliverer];
|
|
170
|
+
|
|
142
171
|
export async function keepOnce(ctx, handle, rec, { log = console.log, session = null, remote: given = null, prepare = null } = {}) {
|
|
143
172
|
const f = await forumFor(ctx, handle, rec);
|
|
144
173
|
if (f.skipped) return { skipped: f.skipped };
|
|
@@ -149,10 +178,19 @@ export async function keepOnce(ctx, handle, rec, { log = console.log, session =
|
|
|
149
178
|
const config = await readConfig(ctx, f, podFetch);
|
|
150
179
|
if (!config) return { skipped: 'no forum on the pod' };
|
|
151
180
|
const copies = await ensureForumCopies(ctx, f, config, say);
|
|
181
|
+
// The forum is held only while its state changes: the drain, the asks, the
|
|
182
|
+
// queue, the heartbeat, the write to the copies. The carry — one signed
|
|
183
|
+
// POST per follower server, each with its own timeout — runs with the forum
|
|
184
|
+
// free, so a post arriving at the door meanwhile is placed at once instead
|
|
185
|
+
// of waiting for the run to end. The queue the carry changes is written
|
|
186
|
+
// back under the lock, merged with whatever the door queued in between.
|
|
152
187
|
const unlock = await lockCopy(ctx.copyKv, f.forumHandle);
|
|
153
188
|
if (!unlock) return { skipped: 'the gateway is busy with this forum', retry: 'soon' };
|
|
189
|
+
let forum;
|
|
190
|
+
let before;
|
|
191
|
+
let asks = 0;
|
|
154
192
|
try {
|
|
155
|
-
|
|
193
|
+
forum = agentFor(ctx, f, { remote, podFetch, copies, log: say, mode: 'run' });
|
|
156
194
|
if (!await forum.connect({ act: false })) return { skipped: 'no forum on the pod' };
|
|
157
195
|
prepare?.(forum); // a test's origins, in place of the network
|
|
158
196
|
if (!await forum.lease.acquire()) return { skipped: 'another host has the forum', retry: 'soon' };
|
|
@@ -161,22 +199,42 @@ export async function keepOnce(ctx, handle, rec, { log = console.log, session =
|
|
|
161
199
|
await forum.intake.drain();
|
|
162
200
|
await forum.applyVerifiedAsks().catch((e) => say(`asks: ${e.message}`));
|
|
163
201
|
await forum.publishModQueue().catch((e) => say(`queue: ${e.message}`));
|
|
164
|
-
for (const cat of forum.categories) await cat.deliverer.drainQueue();
|
|
165
|
-
await forum.siteAgent.deliverer.drainQueue();
|
|
166
202
|
await publish.publishHeartbeat(forum.siteAgent).catch((e) => say(`heartbeat: ${e.message}`));
|
|
167
|
-
await Promise.allSettled(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// The run wrote on the pod (what the door could not place, the queue,
|
|
171
|
-
// the heartbeat): the edge's copies of the forum's documents go.
|
|
172
|
-
if (ctx.purge) {
|
|
173
|
-
const tags = [`u-${f.forumHandle}`, ...(config.categories || []).map((c) => `u-${c.slug}`)];
|
|
174
|
-
await ctx.purge(tags).catch((e) => say(`purge: ${e.message}`));
|
|
175
|
-
}
|
|
176
|
-
say(`done: inbox drained, ${waiting} delivery(ies) waiting, ${asks} ask(s) pending`);
|
|
177
|
-
return { drained: true, waiting, nextAt: nextRun(forum, asks) };
|
|
203
|
+
await Promise.allSettled(storesOf(forum).map((s) => s.flush()));
|
|
204
|
+
asks = storesOf(forum).reduce((n, s) => n + s.read('modqueue.json', []).length, 0);
|
|
205
|
+
before = new Map(storesOf(forum).map((s) => [s, s.getQueue?.() || []]));
|
|
178
206
|
} finally { await forum.lease.release().catch(() => {}); }
|
|
179
207
|
} finally { await unlock(); }
|
|
208
|
+
|
|
209
|
+
// The carry, with the forum free.
|
|
210
|
+
for (const d of deliverersOf(forum)) await d.drainQueue().catch((e) => say(`carry: ${e.message}`));
|
|
211
|
+
const after = new Map(storesOf(forum).map((s) => [s, s.getQueue?.() || []]));
|
|
212
|
+
|
|
213
|
+
// What the carry left, written back under the lock beside what the door
|
|
214
|
+
// queued meanwhile. The door holds the forum for seconds, so the wait is short.
|
|
215
|
+
const relock = await lockCopy(ctx.copyKv, f.forumHandle, { waitMs: 20_000 });
|
|
216
|
+
if (!relock) { say('the queue could not be written back: the forum stayed busy'); return { drained: true, waiting: null, retry: 'soon' }; }
|
|
217
|
+
let waiting = 0;
|
|
218
|
+
try {
|
|
219
|
+
// The copy takes a write only from the lease's holder; the lease again, briefly.
|
|
220
|
+
if (!await forum.lease.acquire()) { say('the queue could not be written back: another host has the forum'); return { drained: true, waiting: null, retry: 'soon' }; }
|
|
221
|
+
for (const s of storesOf(forum)) {
|
|
222
|
+
let fresh = null;
|
|
223
|
+
try { await s.load(); fresh = s.getQueue?.() || []; } catch { fresh = null; }
|
|
224
|
+
const merged = fresh ? mergeQueues(before.get(s) || [], after.get(s) || [], fresh) : (after.get(s) || []);
|
|
225
|
+
s.setQueue?.(merged);
|
|
226
|
+
waiting += merged.length;
|
|
227
|
+
}
|
|
228
|
+
await Promise.allSettled(storesOf(forum).map((s) => s.flush()));
|
|
229
|
+
} finally { await forum.lease.release().catch(() => {}); await relock(); }
|
|
230
|
+
// The run wrote on the pod (what the door could not place, the queue, the
|
|
231
|
+
// heartbeat): the edge's copies of the forum's documents go.
|
|
232
|
+
if (ctx.purge) {
|
|
233
|
+
const tags = [`u-${f.forumHandle}`, ...(config.categories || []).map((c) => `u-${c.slug}`)];
|
|
234
|
+
await ctx.purge(tags).catch((e) => say(`purge: ${e.message}`));
|
|
235
|
+
}
|
|
236
|
+
say(`done: inbox drained, ${waiting} delivery(ies) waiting, ${asks} ask(s) pending`);
|
|
237
|
+
return { drained: true, waiting, nextAt: nextRun(forum, asks) };
|
|
180
238
|
}
|
|
181
239
|
|
|
182
240
|
/**
|
package/src/keep.mjs
CHANGED
|
@@ -12,11 +12,19 @@ import { keepRows } from './setup.mjs';
|
|
|
12
12
|
// does, from the pod's own rules, which name it beside the owner from here
|
|
13
13
|
// on. `on: false` takes the name out again.
|
|
14
14
|
export async function keepForum(forum, { front, on = true }) {
|
|
15
|
-
|
|
16
|
-
forum
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
// Handing over: the categories' rows, then every rule on the tree with the
|
|
16
|
+
// keeper's name, then the forum's own row — whose switch starts the
|
|
17
|
+
// Gateway's first run, which reads the pod under those rules. Taking it
|
|
18
|
+
// back: the rows, then the rules without the name.
|
|
19
|
+
const restate = async (keeper) => {
|
|
20
|
+
forum.keepers = on && keeper ? [keeper] : [];
|
|
21
|
+
forum.remote.keepers = [...forum.keepers];
|
|
22
|
+
forum.remote.aclOwner = forum.readCredential()?.webId || forum.remote.aclOwner || null;
|
|
23
|
+
await forum.restateRules();
|
|
24
|
+
};
|
|
25
|
+
const { keeper, handles } = await keepRows({ fetch: (u, i) => forum.remote.session.fetch(u, i), front, config: forum.config, on,
|
|
26
|
+
between: on ? restate : null });
|
|
27
|
+
if (!on) await restate(null);
|
|
20
28
|
return { keeper: on ? keeper : null, handles, on };
|
|
21
29
|
}
|
|
22
30
|
|
package/src/setup.mjs
CHANGED
|
@@ -99,15 +99,18 @@ export async function attachRows({ fetch: f, front, config, plain }, { log = ()
|
|
|
99
99
|
/**
|
|
100
100
|
* Let a Gateway keep the forum (or stop): every row, the categories first and
|
|
101
101
|
* the forum's own row last, because turning the forum's row on is what starts
|
|
102
|
-
* its first run, and that run wants every category kept already.
|
|
103
|
-
* keeper's WebID
|
|
102
|
+
* its first run, and that run wants every category kept already. `between`,
|
|
103
|
+
* given the keeper's WebID, runs after the categories and before that last
|
|
104
|
+
* switch: the caller names the keeper in the pod's rules there, so the first
|
|
105
|
+
* run can read the pod the moment it starts. Returns the keeper's WebID.
|
|
104
106
|
*/
|
|
105
|
-
export async function keepRows({ fetch: f, front, config, on = true }) {
|
|
107
|
+
export async function keepRows({ fetch: f, front, config, on = true, between = null }) {
|
|
106
108
|
const origin = String(front).replace(/\/+$/u, '');
|
|
107
109
|
const forumHandle = config.handle;
|
|
108
110
|
const handles = [...(config.categories || []).map(c => c.slug), forumHandle];
|
|
109
111
|
let keeper = null;
|
|
110
112
|
for (const handle of handles) {
|
|
113
|
+
if (handle === forumHandle && between) await between(keeper);
|
|
111
114
|
const res = await f(`${origin}/api/keeper`, {
|
|
112
115
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
113
116
|
body: JSON.stringify({ handle, on, ...(handle === forumHandle ? {} : { forum: forumHandle }) }),
|
|
@@ -156,8 +159,8 @@ export async function setUpForumAtGateway({ remote, storageFor, fetch: f, front,
|
|
|
156
159
|
await writeConfig(storage, { ...config, gateway, republish: true });
|
|
157
160
|
onStep('attach', 'ok');
|
|
158
161
|
onStep('keep', 'running');
|
|
159
|
-
const { keeper } = await keepRows({ fetch: f, front, config, on: true
|
|
160
|
-
|
|
162
|
+
const { keeper } = await keepRows({ fetch: f, front, config, on: true,
|
|
163
|
+
between: (k) => nameKeeperInRules(remote, { config, plain, keeper: k, ownerWebId }) });
|
|
161
164
|
onStep('keep', 'ok');
|
|
162
165
|
return { handle: config.handle, front: gateway.front, handles, keeper };
|
|
163
166
|
}
|