@fabriktor/fx 0.0.151 → 0.0.153
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/dist/src/core/err.d.ts +2 -0
- package/dist/src/core/err.js +10 -0
- package/dist/src/core/multi-create.d.ts +1 -0
- package/dist/src/core/multi-create.js +1 -0
- package/dist/src/core/pull.d.ts +27 -2
- package/dist/src/core/pull.js +108 -6
- package/dist/src/core/search-controller.d.ts +1 -0
- package/dist/src/core/search-controller.js +1 -0
- package/dist/src/feed/feed.d.ts +71 -7
- package/dist/src/feed/feed.js +454 -302
- package/dist/src/feed/media.d.ts +53 -0
- package/dist/src/feed/media.js +354 -0
- package/dist/src/feed/validation.d.ts +4 -0
- package/dist/src/feed/validation.js +59 -0
- package/dist/src/fx.js +2 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/marketplace/marketplace.d.ts +64 -5
- package/dist/src/marketplace/marketplace.js +278 -24
- package/dist/src/marketplace/validation.d.ts +11 -0
- package/dist/src/marketplace/validation.js +76 -0
- package/dist/src/routes/departure.d.ts +4 -0
- package/dist/src/routes/departure.js +12 -5
- package/dist/src/storage/storage.d.ts +14 -0
- package/dist/src/storage/storage.js +51 -17
- package/package.json +3 -3
package/dist/src/feed/feed.js
CHANGED
|
@@ -3,19 +3,24 @@
|
|
|
3
3
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
4
|
// not use this file except in compliance with the License. You may obtain
|
|
5
5
|
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
-
import { All,
|
|
7
|
-
import {
|
|
6
|
+
import { All, PullTypeBatch, PullTypeSingle, PullTypeResearch, PullTypeReplies, PullTypeReferences, PullTypeReactions, PullTypeProfilePosts, PullTypeProfileReplies, PullTypeProfileMedias, FeedLike, FeedDislike, FeedReport, PullTypeKeepAlive, ZeroID, } from "@fabriktor/schema";
|
|
7
|
+
import { isClientErr, queryParams, } from "@fabriktor/client";
|
|
8
|
+
import { errResolveFailed, errValidation, errUnconfirmedMutation, } from "../core/err.js";
|
|
8
9
|
import { requiredOperationID, resolvedID } from "../core/id.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { startPullSession, PullSessionScope, } from "../core/pull.js";
|
|
11
|
+
import { runOwnerSearch, } from "../core/search.js";
|
|
12
|
+
import { validateStorageUploadFiles, } from "../storage/storage.js";
|
|
13
|
+
import { createEvent, EventBus } from "../core/events.js";
|
|
14
|
+
import { prepareFeedTextPost, validateFeedVisibility, feedPostInput, } from "./validation.js";
|
|
15
|
+
export * from "./validation.js";
|
|
16
|
+
export * from "./media.js";
|
|
17
|
+
import { saveFeedPost, newFeedPostSaveState, } from "./media.js";
|
|
18
|
+
export const FeedEvents = { Changed: createEvent() };
|
|
12
19
|
const newFeedSessionToken = "";
|
|
13
20
|
const resourceFeedSessionToken = "feed_session_token";
|
|
14
|
-
const resourceFeedPostID = "feed_post_id";
|
|
15
21
|
const resourceFeedPost = "feed_post";
|
|
16
22
|
const resourceFeedReaction = "feed_reaction";
|
|
17
23
|
const resourceFeedFavorite = "feed_favorite";
|
|
18
|
-
const resourceFeedObject = "feed_object";
|
|
19
24
|
const msgKeepAliveIntervalInvalid = "Keep-alive interval must be a finite number greater than zero.";
|
|
20
25
|
const msgFeedSessionClosed = "Feed session is closed.";
|
|
21
26
|
const msgFeedPullInProgress = "A feed pull is already in progress.";
|
|
@@ -36,10 +41,6 @@ const msgFeedReplyAlreadyExists = "A reply already exists for this post.";
|
|
|
36
41
|
const msgFeedReplyMissing = "No reply exists for this post.";
|
|
37
42
|
const msgFeedReferenceAlreadyExists = "A reference already exists for this post.";
|
|
38
43
|
const msgFeedReferenceMissing = "No reference exists for this post.";
|
|
39
|
-
const msgFeedObjectsMissing = "Storage download did not return any feed objects.";
|
|
40
|
-
const msgFeedObjectURLMissing = "Storage object does not contain a public URL.";
|
|
41
|
-
const msgFeedObjectMIMEMissing = "Storage object does not contain a MIME prefix.";
|
|
42
|
-
const msgFeedObjectCaptionMissing = "Storage object does not contain a caption embedding.";
|
|
43
44
|
export const FeedAction = {
|
|
44
45
|
Reaction: "reaction",
|
|
45
46
|
Favorite: "favorite",
|
|
@@ -56,6 +57,9 @@ export const FeedMediaPostStatus = {
|
|
|
56
57
|
PatchFailed: "patch_failed",
|
|
57
58
|
};
|
|
58
59
|
export class FeedFX {
|
|
60
|
+
sessions = new PullSessionScope();
|
|
61
|
+
events;
|
|
62
|
+
mutating = false;
|
|
59
63
|
feed;
|
|
60
64
|
storage_fx;
|
|
61
65
|
keep_alive_interval_ms;
|
|
@@ -66,6 +70,7 @@ export class FeedFX {
|
|
|
66
70
|
constructor(opts) {
|
|
67
71
|
validateFeedFXOptions(opts);
|
|
68
72
|
this.feed = opts.feed;
|
|
73
|
+
this.events = opts.events ?? new EventBus();
|
|
69
74
|
this.storage_fx = opts.storage_fx;
|
|
70
75
|
this.keep_alive_interval_ms = opts.keep_alive_interval_ms;
|
|
71
76
|
this.running_actions = new Map();
|
|
@@ -73,24 +78,231 @@ export class FeedFX {
|
|
|
73
78
|
this.removed_actions = new Map();
|
|
74
79
|
this.action_ids = new Map();
|
|
75
80
|
}
|
|
81
|
+
reset() {
|
|
82
|
+
this.sessions.reset();
|
|
83
|
+
this.running_actions.clear();
|
|
84
|
+
this.completed_actions.clear();
|
|
85
|
+
this.removed_actions.clear();
|
|
86
|
+
this.action_ids.clear();
|
|
87
|
+
}
|
|
76
88
|
async createFeedPost(opts) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
return this.mutate(async (signal) => {
|
|
90
|
+
let id = resolvedID(opts.created_id);
|
|
91
|
+
if (!id) {
|
|
92
|
+
const input = prepareFeedTextPost(opts.in);
|
|
93
|
+
const parent_id = resolvedID(input.reply_to_post_id) ??
|
|
94
|
+
resolvedID(input.referenced_post_id);
|
|
95
|
+
if (parent_id) {
|
|
96
|
+
const parent = await this.requireFeedPost(parent_id, signal);
|
|
97
|
+
const existing = resolvedID(input.reply_to_post_id)
|
|
98
|
+
? parent.self.reply_id
|
|
99
|
+
: parent.self.reference_id;
|
|
100
|
+
// A previous uncertain attempt may already have created the only child.
|
|
101
|
+
if (resolvedID(existing)) {
|
|
102
|
+
const child = await this.findFeedPost(existing, signal);
|
|
103
|
+
if (child.text.text !== input.text.text ||
|
|
104
|
+
child.visibility !== input.visibility ||
|
|
105
|
+
child.reply_to_post_id !== input.reply_to_post_id ||
|
|
106
|
+
child.referenced_post_id !== input.referenced_post_id ||
|
|
107
|
+
child.has_media ||
|
|
108
|
+
child.is_uploading ||
|
|
109
|
+
child.objects.length)
|
|
110
|
+
throw errValidation({
|
|
111
|
+
field: "text",
|
|
112
|
+
msg: "A reply or reference already exists.",
|
|
113
|
+
});
|
|
114
|
+
id = existing;
|
|
115
|
+
opts.on_created?.(id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!id) {
|
|
119
|
+
if (opts.attempted)
|
|
120
|
+
throw errUnconfirmedMutation();
|
|
121
|
+
opts.before_request?.();
|
|
122
|
+
let out;
|
|
123
|
+
try {
|
|
124
|
+
out = await this.feed.insertOneFeedPost({
|
|
125
|
+
in: input,
|
|
126
|
+
signal,
|
|
127
|
+
idempotency_key: opts.idempotency_key,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (isClientErr(error) &&
|
|
132
|
+
(error.status === 400 || error.status === 422))
|
|
133
|
+
opts.on_rejected?.();
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
id = requiredOperationID(out, msgFeedCreatedPostIDMissing);
|
|
137
|
+
opts.on_created?.(id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
signal.throwIfAborted();
|
|
141
|
+
const post = await this.requireFeedPost(id, signal);
|
|
142
|
+
this.changed(post.id);
|
|
143
|
+
if (resolvedID(post.reply_to_post_id))
|
|
144
|
+
this.changed(post.reply_to_post_id);
|
|
145
|
+
if (resolvedID(post.referenced_post_id))
|
|
146
|
+
this.changed(post.referenced_post_id);
|
|
147
|
+
return post;
|
|
148
|
+
}, opts.signal);
|
|
80
149
|
}
|
|
81
150
|
async replaceFeedPost(opts) {
|
|
82
|
-
|
|
83
|
-
|
|
151
|
+
return this.mutate(async (signal) => {
|
|
152
|
+
const current = await this.findFeedPost(opts.id, signal);
|
|
153
|
+
this.checkVersion(current, opts.expected);
|
|
154
|
+
// Text editing preserves link identity and cannot overwrite stored media.
|
|
155
|
+
const input = prepareFeedTextPost({
|
|
156
|
+
...feedPostInput(current),
|
|
157
|
+
...opts.in,
|
|
158
|
+
reply_to_post_id: current.reply_to_post_id,
|
|
159
|
+
referenced_post_id: current.referenced_post_id,
|
|
160
|
+
});
|
|
161
|
+
await this.feed.replaceOneFeedPost({ id: opts.id, in: input, signal });
|
|
162
|
+
signal.throwIfAborted();
|
|
163
|
+
this.changed(opts.id);
|
|
164
|
+
return this.requireFeedPost(opts.id, signal);
|
|
165
|
+
}, opts.signal);
|
|
84
166
|
}
|
|
85
167
|
async searchFeedPosts(opts) {
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
168
|
+
return runOwnerSearch({
|
|
169
|
+
...opts,
|
|
170
|
+
search: (input) => this.feed.searchManyFeedPosts(input),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async readOwnReaction(opts) {
|
|
174
|
+
const signal = this.sessions.signal(opts.signal);
|
|
175
|
+
signal.throwIfAborted();
|
|
176
|
+
const id = requireFeedID(opts.id);
|
|
177
|
+
try {
|
|
178
|
+
const out = await this.feed.findOneFeedReaction({ id, signal });
|
|
179
|
+
signal.throwIfAborted();
|
|
180
|
+
return out.value ?? null;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
signal.throwIfAborted();
|
|
184
|
+
if (isClientErr(error) && (error.status === 403 || error.status === 404))
|
|
185
|
+
return null;
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async readFeedPost(opts) {
|
|
190
|
+
const signal = this.sessions.signal(opts.signal);
|
|
191
|
+
signal.throwIfAborted();
|
|
192
|
+
const id = requireFeedID(opts.id);
|
|
193
|
+
try {
|
|
194
|
+
const out = await this.feed.pull({
|
|
195
|
+
contact_ids: [],
|
|
196
|
+
session_token: "",
|
|
197
|
+
is_new_session: true,
|
|
198
|
+
pull_type: PullTypeSingle,
|
|
199
|
+
entity_id: id,
|
|
200
|
+
research: "",
|
|
201
|
+
signal,
|
|
202
|
+
});
|
|
203
|
+
signal.throwIfAborted();
|
|
204
|
+
return out.value?.posts.find((post) => post.id === id) ?? null;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
signal.throwIfAborted();
|
|
208
|
+
if (isClientErr(error) && (error.status === 403 || error.status === 404))
|
|
209
|
+
return null;
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async listSavedFeedPosts(opts) {
|
|
214
|
+
const signal = this.sessions.signal(opts.signal);
|
|
215
|
+
signal.throwIfAborted();
|
|
216
|
+
const out = await this.feed.queryFeedFavorites({
|
|
217
|
+
query: queryParams({
|
|
218
|
+
owner_id: requireFeedID(opts.owner_id),
|
|
219
|
+
}),
|
|
220
|
+
signal,
|
|
221
|
+
});
|
|
222
|
+
signal.throwIfAborted();
|
|
223
|
+
const result = [];
|
|
224
|
+
// This backend collection exposes an unpaged query, not a search route.
|
|
225
|
+
for (const favorite of out.value ?? []) {
|
|
226
|
+
const post = await this.readFeedPost({ id: favorite.post_id, signal });
|
|
227
|
+
signal.throwIfAborted();
|
|
228
|
+
result.push({ ...favorite, post });
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
async removeSavedFeedPost(opts) {
|
|
233
|
+
return this.mutate(async (signal) => {
|
|
234
|
+
const out = await this.feed.findOneFeedFavorite({
|
|
235
|
+
id: requireFeedID(opts.id),
|
|
236
|
+
signal,
|
|
237
|
+
});
|
|
238
|
+
signal.throwIfAborted();
|
|
239
|
+
if (!out.value || out.value.owner_id !== requireFeedID(opts.owner_id))
|
|
240
|
+
throw errValidation({ msg: "Favorite is unavailable." });
|
|
241
|
+
await this.feed.deleteOneFeedFavorite({ id: opts.id, signal });
|
|
242
|
+
signal.throwIfAborted();
|
|
243
|
+
this.changed(out.value.post_id);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
async deleteFeedPost(opts) {
|
|
247
|
+
return this.mutate(async (signal) => {
|
|
248
|
+
const current = await this.findFeedPost(requireFeedID(opts.record.id), signal);
|
|
249
|
+
this.checkVersion(current, {
|
|
250
|
+
...opts.record,
|
|
251
|
+
owner_id: requireFeedID(opts.owner_id),
|
|
252
|
+
});
|
|
253
|
+
await this.feed.deleteOneFeedPost({ id: current.id, signal });
|
|
254
|
+
signal.throwIfAborted();
|
|
255
|
+
this.changed(current.id);
|
|
256
|
+
if (resolvedID(current.reply_to_post_id))
|
|
257
|
+
this.changed(current.reply_to_post_id);
|
|
258
|
+
if (resolvedID(current.referenced_post_id))
|
|
259
|
+
this.changed(current.referenced_post_id);
|
|
92
260
|
});
|
|
93
261
|
}
|
|
262
|
+
async restrictFeed(opts) {
|
|
263
|
+
validateFeedVisibility(opts.new_visibility);
|
|
264
|
+
return this.mutate(async (signal) => {
|
|
265
|
+
await this.feed.restrict({ ...opts, signal });
|
|
266
|
+
signal.throwIfAborted();
|
|
267
|
+
this.changed("");
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
checkVersion(current, expected) {
|
|
271
|
+
if (expected &&
|
|
272
|
+
(current.owner_id !== expected.owner_id ||
|
|
273
|
+
current.updated_at !== expected.updated_at))
|
|
274
|
+
throw errValidation({
|
|
275
|
+
field: "updated_at",
|
|
276
|
+
msg: "Reload the post before changing it.",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
async requireFeedPost(id, signal) {
|
|
280
|
+
const post = await this.readFeedPost({ id, signal });
|
|
281
|
+
if (!post)
|
|
282
|
+
throw errResolveFailed({
|
|
283
|
+
resource: resourceFeedPost,
|
|
284
|
+
msg: msgFeedPostMissing,
|
|
285
|
+
});
|
|
286
|
+
return post;
|
|
287
|
+
}
|
|
288
|
+
changed(post_id) {
|
|
289
|
+
this.events.publish(FeedEvents.Changed, { post_id });
|
|
290
|
+
}
|
|
291
|
+
async mutate(fn, parent) {
|
|
292
|
+
if (this.mutating)
|
|
293
|
+
throw errValidation({ msg: msgFeedActionInProgress });
|
|
294
|
+
const signal = this.sessions.signal(parent);
|
|
295
|
+
signal.throwIfAborted();
|
|
296
|
+
this.mutating = true;
|
|
297
|
+
try {
|
|
298
|
+
const result = await fn(signal);
|
|
299
|
+
signal.throwIfAborted();
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
this.mutating = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
94
306
|
async createFeedReaction(opts) {
|
|
95
307
|
const out = await this.feed.insertOneFeedReaction(opts);
|
|
96
308
|
const id = requiredOperationID(out, msgCreatedFeedReactionIDMissing);
|
|
@@ -110,28 +322,54 @@ export class FeedFX {
|
|
|
110
322
|
return await this.findFeedFavorite(opts.id);
|
|
111
323
|
}
|
|
112
324
|
async startSession(opts) {
|
|
113
|
-
const { on_error, ...
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
325
|
+
const { on_error, signal: parentSignal, ...input } = opts;
|
|
326
|
+
const supported = [
|
|
327
|
+
PullTypeBatch,
|
|
328
|
+
PullTypeResearch,
|
|
329
|
+
PullTypeReactions,
|
|
330
|
+
PullTypeReferences,
|
|
331
|
+
PullTypeReplies,
|
|
332
|
+
PullTypeProfilePosts,
|
|
333
|
+
PullTypeProfileReplies,
|
|
334
|
+
PullTypeProfileMedias,
|
|
335
|
+
];
|
|
336
|
+
if (!supported.includes(input.pull_type))
|
|
337
|
+
throw errValidation({
|
|
338
|
+
field: "pull_type",
|
|
339
|
+
msg: "Unsupported Feed session.",
|
|
340
|
+
});
|
|
341
|
+
if (input.pull_type === PullTypeResearch && !input.research.trim())
|
|
342
|
+
throw errValidation({ field: "research", msg: "Research is required." });
|
|
343
|
+
if (![PullTypeBatch, PullTypeResearch].includes(input.pull_type))
|
|
344
|
+
requireFeedID(input.entity_id);
|
|
345
|
+
const context = {
|
|
346
|
+
...input,
|
|
347
|
+
research: input.research.trim(),
|
|
348
|
+
contact_ids: [...input.contact_ids],
|
|
349
|
+
};
|
|
350
|
+
const signal = this.sessions.signal(parentSignal);
|
|
351
|
+
return await startPullSession({
|
|
352
|
+
signal,
|
|
353
|
+
pull_initial: (signal) => this.feed.pull({
|
|
354
|
+
...context,
|
|
355
|
+
session_token: newFeedSessionToken,
|
|
356
|
+
is_new_session: true,
|
|
357
|
+
signal,
|
|
358
|
+
}),
|
|
123
359
|
keep_alive_interval_ms: this.keep_alive_interval_ms,
|
|
124
|
-
pull_next: async (token) => {
|
|
360
|
+
pull_next: async (token, signal) => {
|
|
125
361
|
return await this.feed.pull({
|
|
126
362
|
...context,
|
|
127
363
|
session_token: token,
|
|
364
|
+
signal,
|
|
128
365
|
is_new_session: false,
|
|
129
366
|
});
|
|
130
367
|
},
|
|
131
|
-
keep_alive: async (token) => {
|
|
368
|
+
keep_alive: async (token, signal) => {
|
|
132
369
|
return await this.feed.pull({
|
|
133
370
|
contact_ids: [],
|
|
134
371
|
session_token: token,
|
|
372
|
+
signal,
|
|
135
373
|
is_new_session: false,
|
|
136
374
|
pull_type: PullTypeKeepAlive,
|
|
137
375
|
entity_id: "",
|
|
@@ -150,85 +388,90 @@ export class FeedFX {
|
|
|
150
388
|
}
|
|
151
389
|
async addReaction(opts) {
|
|
152
390
|
const post_id = feedPostID(opts.post);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
391
|
+
return await this.runAction(post_id, FeedAction.Reaction, async (signal) => {
|
|
392
|
+
const fresh = await this.requireFeedPost(post_id, signal);
|
|
393
|
+
if (resolvedID(fresh.self.reaction_id))
|
|
394
|
+
throw errValidation({
|
|
395
|
+
msg: "An action already exists for this post.",
|
|
396
|
+
});
|
|
397
|
+
if (![FeedLike, FeedDislike, FeedReport].includes(opts.reaction_type))
|
|
398
|
+
throw errValidation({
|
|
399
|
+
field: "reaction_type",
|
|
400
|
+
msg: "Unsupported reaction.",
|
|
401
|
+
});
|
|
160
402
|
const out = await this.feed.insertOneFeedReaction({
|
|
403
|
+
signal,
|
|
161
404
|
in: {
|
|
162
405
|
post_id,
|
|
163
406
|
reaction_type: opts.reaction_type,
|
|
164
407
|
},
|
|
165
408
|
});
|
|
409
|
+
signal.throwIfAborted();
|
|
166
410
|
this.markAction(post_id, FeedAction.Reaction, operationID(out));
|
|
167
411
|
return out;
|
|
168
412
|
});
|
|
169
413
|
}
|
|
170
414
|
async removeReaction(opts) {
|
|
171
415
|
const post_id = feedPostID(opts.post);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
msg:
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
return await this.runAction(post_id, FeedAction.Reaction, async () => {
|
|
416
|
+
return await this.runAction(post_id, FeedAction.Reaction, async (signal) => {
|
|
417
|
+
const fresh = await this.requireFeedPost(post_id, signal);
|
|
418
|
+
const id = resolvedID(fresh.self.reaction_id);
|
|
419
|
+
if (!id)
|
|
420
|
+
throw errValidation({ msg: "This action is no longer present." });
|
|
180
421
|
const out = await this.feed.deleteOneFeedReaction({
|
|
422
|
+
signal,
|
|
181
423
|
id,
|
|
182
424
|
});
|
|
425
|
+
signal.throwIfAborted();
|
|
183
426
|
this.markActionRemoved(post_id, FeedAction.Reaction);
|
|
184
427
|
return out;
|
|
185
428
|
});
|
|
186
429
|
}
|
|
187
430
|
async addFavorite(opts) {
|
|
188
431
|
const post_id = feedPostID(opts.post);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return await this.runAction(post_id, FeedAction.Favorite, async () => {
|
|
432
|
+
return await this.runAction(post_id, FeedAction.Favorite, async (signal) => {
|
|
433
|
+
const fresh = await this.requireFeedPost(post_id, signal);
|
|
434
|
+
if (resolvedID(fresh.self.favorite_id))
|
|
435
|
+
throw errValidation({
|
|
436
|
+
msg: "An action already exists for this post.",
|
|
437
|
+
});
|
|
196
438
|
const out = await this.feed.insertOneFeedFavorite({
|
|
439
|
+
signal,
|
|
197
440
|
in: {
|
|
198
441
|
post_id,
|
|
199
442
|
},
|
|
200
443
|
});
|
|
444
|
+
signal.throwIfAborted();
|
|
201
445
|
this.markAction(post_id, FeedAction.Favorite, operationID(out));
|
|
202
446
|
return out;
|
|
203
447
|
});
|
|
204
448
|
}
|
|
205
449
|
async removeFavorite(opts) {
|
|
206
450
|
const post_id = feedPostID(opts.post);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
msg:
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
return await this.runAction(post_id, FeedAction.Favorite, async () => {
|
|
451
|
+
return await this.runAction(post_id, FeedAction.Favorite, async (signal) => {
|
|
452
|
+
const fresh = await this.requireFeedPost(post_id, signal);
|
|
453
|
+
const id = resolvedID(fresh.self.favorite_id);
|
|
454
|
+
if (!id)
|
|
455
|
+
throw errValidation({ msg: "This action is no longer present." });
|
|
215
456
|
const out = await this.feed.deleteOneFeedFavorite({
|
|
457
|
+
signal,
|
|
216
458
|
id,
|
|
217
459
|
});
|
|
460
|
+
signal.throwIfAborted();
|
|
218
461
|
this.markActionRemoved(post_id, FeedAction.Favorite);
|
|
219
462
|
return out;
|
|
220
463
|
});
|
|
221
464
|
}
|
|
222
465
|
async createReply(opts) {
|
|
223
466
|
const post_id = feedPostID(opts.post);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
msg:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return await this.runAction(post_id, FeedAction.Reply, async () => {
|
|
467
|
+
return await this.runAction(post_id, FeedAction.Reply, async (signal) => {
|
|
468
|
+
const fresh = await this.requireFeedPost(post_id, signal);
|
|
469
|
+
if (resolvedID(fresh.self.reply_id))
|
|
470
|
+
throw errValidation({ msg: "An action already exists for this post." });
|
|
471
|
+
if (!opts.in.text.text.trim())
|
|
472
|
+
throw errValidation({ field: "text", msg: "Reply text is required." });
|
|
231
473
|
const out = await this.feed.insertOneFeedPost({
|
|
474
|
+
signal,
|
|
232
475
|
in: {
|
|
233
476
|
objects: [],
|
|
234
477
|
text: opts.in.text,
|
|
@@ -239,37 +482,37 @@ export class FeedFX {
|
|
|
239
482
|
reply_to_post_id: post_id,
|
|
240
483
|
},
|
|
241
484
|
});
|
|
485
|
+
signal.throwIfAborted();
|
|
242
486
|
this.markAction(post_id, FeedAction.Reply, operationID(out));
|
|
243
487
|
return out;
|
|
244
488
|
});
|
|
245
489
|
}
|
|
246
490
|
async removeReply(opts) {
|
|
247
491
|
const post_id = feedPostID(opts.post);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
msg:
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
return await this.runAction(post_id, FeedAction.Reply, async () => {
|
|
492
|
+
return await this.runAction(post_id, FeedAction.Reply, async (signal) => {
|
|
493
|
+
const fresh = await this.requireFeedPost(post_id, signal);
|
|
494
|
+
const id = resolvedID(fresh.self.reply_id);
|
|
495
|
+
if (!id)
|
|
496
|
+
throw errValidation({ msg: "This action is no longer present." });
|
|
256
497
|
const out = await this.feed.deleteOneFeedPost({
|
|
498
|
+
signal,
|
|
257
499
|
id,
|
|
258
500
|
});
|
|
501
|
+
signal.throwIfAborted();
|
|
259
502
|
this.markActionRemoved(post_id, FeedAction.Reply);
|
|
260
503
|
return out;
|
|
261
504
|
});
|
|
262
505
|
}
|
|
263
506
|
async createReference(opts) {
|
|
264
507
|
const post_id = feedPostID(opts.post);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return await this.runAction(post_id, FeedAction.Reference, async () => {
|
|
508
|
+
return await this.runAction(post_id, FeedAction.Reference, async (signal) => {
|
|
509
|
+
const fresh = await this.requireFeedPost(post_id, signal);
|
|
510
|
+
if (resolvedID(fresh.self.reference_id))
|
|
511
|
+
throw errValidation({
|
|
512
|
+
msg: "An action already exists for this post.",
|
|
513
|
+
});
|
|
272
514
|
const out = await this.feed.insertOneFeedPost({
|
|
515
|
+
signal,
|
|
273
516
|
in: {
|
|
274
517
|
objects: [],
|
|
275
518
|
text: opts.in.text,
|
|
@@ -280,208 +523,151 @@ export class FeedFX {
|
|
|
280
523
|
reply_to_post_id: ZeroID,
|
|
281
524
|
},
|
|
282
525
|
});
|
|
526
|
+
signal.throwIfAborted();
|
|
283
527
|
this.markAction(post_id, FeedAction.Reference, operationID(out));
|
|
284
528
|
return out;
|
|
285
529
|
});
|
|
286
530
|
}
|
|
287
531
|
async removeReference(opts) {
|
|
288
532
|
const post_id = feedPostID(opts.post);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
msg:
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
return await this.runAction(post_id, FeedAction.Reference, async () => {
|
|
533
|
+
return await this.runAction(post_id, FeedAction.Reference, async (signal) => {
|
|
534
|
+
const fresh = await this.requireFeedPost(post_id, signal);
|
|
535
|
+
const id = resolvedID(fresh.self.reference_id);
|
|
536
|
+
if (!id)
|
|
537
|
+
throw errValidation({ msg: "This action is no longer present." });
|
|
297
538
|
const out = await this.feed.deleteOneFeedPost({
|
|
539
|
+
signal,
|
|
298
540
|
id,
|
|
299
541
|
});
|
|
542
|
+
signal.throwIfAborted();
|
|
300
543
|
this.markActionRemoved(post_id, FeedAction.Reference);
|
|
301
544
|
return out;
|
|
302
545
|
});
|
|
303
546
|
}
|
|
547
|
+
async saveFeedPost(opts) {
|
|
548
|
+
return this.mutate((signal) => saveFeedPost({
|
|
549
|
+
feed: this.feed,
|
|
550
|
+
storage: this.storage_fx,
|
|
551
|
+
read: (id, signal) => this.requireFeedPost(id, signal),
|
|
552
|
+
changed: (id) => this.changed(id),
|
|
553
|
+
}, { ...opts, signal }), opts.signal);
|
|
554
|
+
}
|
|
304
555
|
async createPostWithMedia(opts) {
|
|
305
|
-
return
|
|
556
|
+
return this.createMediaPost({
|
|
557
|
+
...opts,
|
|
306
558
|
in: {
|
|
559
|
+
...opts.in,
|
|
307
560
|
objects: [],
|
|
308
|
-
text: opts.in.text,
|
|
309
|
-
is_uploading: true,
|
|
310
561
|
has_media: true,
|
|
311
|
-
|
|
312
|
-
referenced_post_id: ZeroID,
|
|
562
|
+
is_uploading: true,
|
|
313
563
|
reply_to_post_id: ZeroID,
|
|
564
|
+
referenced_post_id: ZeroID,
|
|
314
565
|
},
|
|
315
|
-
user_id: opts.user_id,
|
|
316
|
-
uploader_id: opts.uploader_id,
|
|
317
|
-
full_quality: opts.full_quality,
|
|
318
|
-
files: opts.files,
|
|
319
|
-
save_to_personal_box: opts.save_to_personal_box,
|
|
320
566
|
});
|
|
321
567
|
}
|
|
322
568
|
async createReplyWithMedia(opts) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
text: opts.in.text,
|
|
335
|
-
is_uploading: true,
|
|
336
|
-
has_media: true,
|
|
337
|
-
visibility: All,
|
|
338
|
-
referenced_post_id: ZeroID,
|
|
339
|
-
reply_to_post_id: post_id,
|
|
340
|
-
},
|
|
341
|
-
user_id: opts.user_id,
|
|
342
|
-
uploader_id: opts.uploader_id,
|
|
343
|
-
full_quality: opts.full_quality,
|
|
344
|
-
files: opts.files,
|
|
345
|
-
save_to_personal_box: opts.save_to_personal_box,
|
|
346
|
-
});
|
|
347
|
-
const id = completedMediaPostID(out);
|
|
348
|
-
if (id !== undefined) {
|
|
349
|
-
this.markAction(post_id, FeedAction.Reply, id);
|
|
350
|
-
}
|
|
351
|
-
return out;
|
|
569
|
+
return this.createMediaPost({
|
|
570
|
+
...opts,
|
|
571
|
+
in: {
|
|
572
|
+
...opts.in,
|
|
573
|
+
visibility: All,
|
|
574
|
+
objects: [],
|
|
575
|
+
has_media: true,
|
|
576
|
+
is_uploading: true,
|
|
577
|
+
reply_to_post_id: feedPostID(opts.post),
|
|
578
|
+
referenced_post_id: ZeroID,
|
|
579
|
+
},
|
|
352
580
|
});
|
|
353
581
|
}
|
|
354
582
|
async createReferenceWithMedia(opts) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
text: opts.in.text,
|
|
367
|
-
is_uploading: true,
|
|
368
|
-
has_media: true,
|
|
369
|
-
visibility: All,
|
|
370
|
-
referenced_post_id: post_id,
|
|
371
|
-
reply_to_post_id: ZeroID,
|
|
372
|
-
},
|
|
373
|
-
user_id: opts.user_id,
|
|
374
|
-
uploader_id: opts.uploader_id,
|
|
375
|
-
full_quality: opts.full_quality,
|
|
376
|
-
files: opts.files,
|
|
377
|
-
save_to_personal_box: opts.save_to_personal_box,
|
|
378
|
-
});
|
|
379
|
-
const id = completedMediaPostID(out);
|
|
380
|
-
if (id !== undefined) {
|
|
381
|
-
this.markAction(post_id, FeedAction.Reference, id);
|
|
382
|
-
}
|
|
383
|
-
return out;
|
|
583
|
+
return this.createMediaPost({
|
|
584
|
+
...opts,
|
|
585
|
+
in: {
|
|
586
|
+
...opts.in,
|
|
587
|
+
visibility: All,
|
|
588
|
+
objects: [],
|
|
589
|
+
has_media: true,
|
|
590
|
+
is_uploading: true,
|
|
591
|
+
reply_to_post_id: ZeroID,
|
|
592
|
+
referenced_post_id: feedPostID(opts.post),
|
|
593
|
+
},
|
|
384
594
|
});
|
|
385
595
|
}
|
|
386
596
|
async createMediaPost(opts) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
const post_id = operationID(create);
|
|
391
|
-
if (post_id === undefined) {
|
|
392
|
-
return {
|
|
393
|
-
status: FeedMediaPostStatus.CreateUnresolved,
|
|
394
|
-
create,
|
|
395
|
-
error: errResolveFailed({
|
|
396
|
-
resource: resourceFeedPostID,
|
|
397
|
-
msg: msgFeedCreatedPostIDMissing,
|
|
398
|
-
}),
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
let upload;
|
|
402
|
-
try {
|
|
403
|
-
upload = await this.storage_fx.uploadObjectsWithPersonalBox({
|
|
404
|
-
col: ColStorageFeed,
|
|
405
|
-
owner_id: post_id,
|
|
406
|
-
user_id: opts.user_id,
|
|
407
|
-
uploader_id: opts.uploader_id,
|
|
408
|
-
full_quality: opts.full_quality,
|
|
409
|
-
public_url: true,
|
|
410
|
-
files: opts.files,
|
|
411
|
-
save_to_personal_box: opts.save_to_personal_box,
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
catch (err) {
|
|
415
|
-
return {
|
|
416
|
-
status: FeedMediaPostStatus.UploadFailed,
|
|
417
|
-
post_id,
|
|
418
|
-
error: err,
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
if (upload.primary.moderation === StorageUploadModeration.AllForbidden) {
|
|
422
|
-
return {
|
|
423
|
-
status: FeedMediaPostStatus.AllForbidden,
|
|
424
|
-
post_id,
|
|
425
|
-
upload,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
let stored;
|
|
597
|
+
validateStorageUploadFiles(opts.files);
|
|
598
|
+
const state = opts.state ?? newFeedPostSaveState();
|
|
599
|
+
const progress = { stage: "create" };
|
|
429
600
|
try {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
601
|
+
const result = await this.saveFeedPost({
|
|
602
|
+
...opts,
|
|
603
|
+
state,
|
|
604
|
+
owner_id: opts.user_id ?? opts.uploader_id,
|
|
605
|
+
signal: this.sessions.signal(opts.signal),
|
|
606
|
+
idempotency_key: opts.idempotency_key ?? state.create_key,
|
|
607
|
+
on_stage: (value) => {
|
|
608
|
+
progress.stage = value;
|
|
609
|
+
},
|
|
610
|
+
draft: {
|
|
611
|
+
in: opts.in,
|
|
612
|
+
retained_urls: [],
|
|
613
|
+
files: opts.files,
|
|
614
|
+
full_quality: opts.full_quality ?? false,
|
|
615
|
+
save_to_personal_box: opts.save_to_personal_box ?? false,
|
|
616
|
+
},
|
|
433
617
|
});
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
objects = feedObjects(stored.value ?? []);
|
|
446
|
-
}
|
|
447
|
-
catch (err) {
|
|
618
|
+
if (!state.post_id || !result.upload)
|
|
619
|
+
throw errResolveFailed({
|
|
620
|
+
resource: resourceFeedPost,
|
|
621
|
+
msg: msgFeedPostMissing,
|
|
622
|
+
});
|
|
623
|
+
if (!result.post)
|
|
624
|
+
return {
|
|
625
|
+
status: FeedMediaPostStatus.AllForbidden,
|
|
626
|
+
post_id: state.post_id,
|
|
627
|
+
upload: result.upload,
|
|
628
|
+
};
|
|
448
629
|
return {
|
|
449
|
-
status: FeedMediaPostStatus.
|
|
450
|
-
post_id,
|
|
451
|
-
|
|
452
|
-
|
|
630
|
+
status: FeedMediaPostStatus.Complete,
|
|
631
|
+
post_id: result.post.id,
|
|
632
|
+
objects: result.post.objects,
|
|
633
|
+
upload: result.upload,
|
|
453
634
|
};
|
|
454
635
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
636
|
+
catch (error) {
|
|
637
|
+
opts.signal?.throwIfAborted();
|
|
638
|
+
if (!state.post_id)
|
|
639
|
+
throw error;
|
|
640
|
+
if (!state.upload)
|
|
641
|
+
return {
|
|
642
|
+
status: FeedMediaPostStatus.UploadFailed,
|
|
643
|
+
post_id: state.post_id,
|
|
644
|
+
error,
|
|
645
|
+
};
|
|
646
|
+
if (state.input)
|
|
647
|
+
return {
|
|
648
|
+
status: FeedMediaPostStatus.PatchFailed,
|
|
649
|
+
post_id: state.post_id,
|
|
650
|
+
upload: state.upload,
|
|
651
|
+
objects: state.input.objects,
|
|
652
|
+
error,
|
|
653
|
+
};
|
|
466
654
|
return {
|
|
467
|
-
status:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
655
|
+
status: progress.stage === "resolve"
|
|
656
|
+
? FeedMediaPostStatus.ResolveFailed
|
|
657
|
+
: FeedMediaPostStatus.DownloadFailed,
|
|
658
|
+
post_id: state.post_id,
|
|
659
|
+
upload: state.upload,
|
|
660
|
+
error,
|
|
472
661
|
};
|
|
473
662
|
}
|
|
474
|
-
return {
|
|
475
|
-
status: FeedMediaPostStatus.Complete,
|
|
476
|
-
post_id,
|
|
477
|
-
upload,
|
|
478
|
-
objects,
|
|
479
|
-
};
|
|
480
663
|
}
|
|
481
|
-
async findFeedPost(id) {
|
|
664
|
+
async findFeedPost(id, signal) {
|
|
665
|
+
signal?.throwIfAborted();
|
|
482
666
|
const out = await this.feed.findOneFeedPost({
|
|
483
667
|
id,
|
|
668
|
+
signal,
|
|
484
669
|
});
|
|
670
|
+
signal?.throwIfAborted();
|
|
485
671
|
const post = out.value;
|
|
486
672
|
if (post === undefined) {
|
|
487
673
|
throw errResolveFailed({
|
|
@@ -518,18 +704,19 @@ export class FeedFX {
|
|
|
518
704
|
return favorite;
|
|
519
705
|
}
|
|
520
706
|
async runAction(post_id, action, fn) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
707
|
+
return this.mutate(async (signal) => {
|
|
708
|
+
this.setActionRunning(post_id, action, true);
|
|
709
|
+
try {
|
|
710
|
+
const result = await fn(signal);
|
|
711
|
+
signal.throwIfAborted();
|
|
712
|
+
this.changed(post_id);
|
|
713
|
+
return result;
|
|
714
|
+
}
|
|
715
|
+
finally {
|
|
716
|
+
if (!signal.aborted)
|
|
717
|
+
this.setActionRunning(post_id, action, false);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
533
720
|
}
|
|
534
721
|
hasAction(post, action) {
|
|
535
722
|
const post_id = feedPostID(post);
|
|
@@ -674,47 +861,12 @@ function resolvedValue(v) {
|
|
|
674
861
|
}
|
|
675
862
|
return out;
|
|
676
863
|
}
|
|
677
|
-
function
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
function feedObjects(objects) {
|
|
684
|
-
if (objects.length === 0) {
|
|
685
|
-
throw errResolveFailed({
|
|
686
|
-
resource: resourceFeedObject,
|
|
687
|
-
msg: msgFeedObjectsMissing,
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
const out = [];
|
|
691
|
-
for (const object of objects) {
|
|
692
|
-
const public_url = resolvedValue(object.public_url);
|
|
693
|
-
if (public_url === undefined) {
|
|
694
|
-
throw errResolveFailed({
|
|
695
|
-
resource: resourceFeedObject,
|
|
696
|
-
msg: msgFeedObjectURLMissing,
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
const mime_prefix = resolvedValue(object.mime_prefix);
|
|
700
|
-
if (mime_prefix === undefined) {
|
|
701
|
-
throw errResolveFailed({
|
|
702
|
-
resource: resourceFeedObject,
|
|
703
|
-
msg: msgFeedObjectMIMEMissing,
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
const embedding = object.caption;
|
|
707
|
-
if (embedding === undefined) {
|
|
708
|
-
throw errResolveFailed({
|
|
709
|
-
resource: resourceFeedObject,
|
|
710
|
-
msg: msgFeedObjectCaptionMissing,
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
out.push({
|
|
714
|
-
public_url,
|
|
715
|
-
mime_prefix,
|
|
716
|
-
embedding,
|
|
864
|
+
function requireFeedID(value) {
|
|
865
|
+
const id = resolvedID(value);
|
|
866
|
+
if (!id)
|
|
867
|
+
throw errValidation({
|
|
868
|
+
field: "id",
|
|
869
|
+
msg: "A Feed record or owner ID is required.",
|
|
717
870
|
});
|
|
718
|
-
|
|
719
|
-
return out;
|
|
871
|
+
return id;
|
|
720
872
|
}
|