@fabriktor/fx 0.0.152 → 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.
@@ -49,5 +49,7 @@ export declare function errValidation<T extends object = Record<string, unknown>
49
49
  export declare function errResolveFailed<T extends object = Record<string, unknown>>(opts: ResolveFailedFXErrOptions<T>): FXErr;
50
50
  export declare function errUnknown(cause: unknown): FXErr;
51
51
  export declare function isFXErr(err: unknown): err is FXErr;
52
+ export declare function errUnconfirmedMutation(): FXErr;
53
+ export declare function isUnconfirmedMutation(err: unknown): boolean;
52
54
  export declare function fxErrMessage(err: unknown): string;
53
55
  export {};
@@ -4,6 +4,7 @@
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
6
  const msgUnknownFXErr = "Unknown FX error.";
7
+ const resourceUnconfirmedMutation = "unconfirmed_mutation";
7
8
  export const FXErrKind = {
8
9
  Required: "required",
9
10
  Validation: "validation",
@@ -78,6 +79,15 @@ export function errUnknown(cause) {
78
79
  export function isFXErr(err) {
79
80
  return err instanceof FXErr;
80
81
  }
82
+ export function errUnconfirmedMutation() {
83
+ return errResolveFailed({
84
+ resource: resourceUnconfirmedMutation,
85
+ msg: "The previous mutation was not acknowledged. Repeating it could duplicate the operation.",
86
+ });
87
+ }
88
+ export function isUnconfirmedMutation(err) {
89
+ return isFXErr(err) && err.resource === resourceUnconfirmedMutation;
90
+ }
81
91
  export function fxErrMessage(err) {
82
92
  if (isFXErr(err)) {
83
93
  return err.message;
@@ -9,6 +9,7 @@ export declare const MultiCreateStatus: {
9
9
  };
10
10
  export type MultiCreateStatus = (typeof MultiCreateStatus)[keyof typeof MultiCreateStatus];
11
11
  export type MultiCreateCheckpoint = {
12
+ attempted?: boolean;
12
13
  created_id?: string;
13
14
  on_created?: (id: string) => void;
14
15
  on_rejected?: () => void;
@@ -194,6 +194,7 @@ export class MultiCreator {
194
194
  signal,
195
195
  idempotency_key: current.idempotency_key,
196
196
  created_id: current.created_id,
197
+ attempted: current.attempted,
197
198
  before_request: () => {
198
199
  signal.throwIfAborted();
199
200
  this.opts.before_request?.();
@@ -1,3 +1,4 @@
1
+ import { type SearchOptions, type SearchPage } from "./search.js";
1
2
  export type PullSessionErrorHandler = (err: unknown) => void;
2
3
  export interface PullSession<TResult> {
3
4
  readonly initial: TResult;
@@ -9,13 +10,23 @@ export type PullSessionOptions<TResult> = {
9
10
  initial: TResult;
10
11
  session_token: string;
11
12
  keep_alive_interval_ms: number;
12
- pull_next: (session_token: string) => Promise<TResult>;
13
- keep_alive: (session_token: string) => Promise<TResult>;
13
+ pull_next: (session_token: string, signal: AbortSignal) => Promise<TResult>;
14
+ keep_alive: (session_token: string, signal: AbortSignal) => Promise<TResult>;
14
15
  resolve_session_token: (out: TResult) => string;
15
16
  closed_error: () => Error;
16
17
  pull_in_progress_error: () => Error;
17
18
  on_error?: PullSessionErrorHandler;
19
+ signal?: AbortSignal;
18
20
  };
21
+ export type StartPullSessionOptions<TResult> = Omit<PullSessionOptions<TResult>, "initial" | "session_token"> & {
22
+ pull_initial: (signal?: AbortSignal) => Promise<TResult>;
23
+ };
24
+ export declare class PullSessionScope {
25
+ private abort;
26
+ signal(signal?: AbortSignal): AbortSignal;
27
+ reset(): void;
28
+ }
29
+ export declare function startPullSession<TResult>(opts: StartPullSessionOptions<TResult>): Promise<DefaultPullSession<TResult>>;
19
30
  export declare class DefaultPullSession<TResult> implements PullSession<TResult> {
20
31
  readonly initial: TResult;
21
32
  private token;
@@ -29,6 +40,9 @@ export declare class DefaultPullSession<TResult> implements PullSession<TResult>
29
40
  private closedError;
30
41
  private pullInProgressError;
31
42
  private onError?;
43
+ private readonly abort;
44
+ private readonly parentSignal?;
45
+ private readonly onAbort;
32
46
  constructor(opts: PullSessionOptions<TResult>);
33
47
  get sessionToken(): string;
34
48
  pullNext(): Promise<TResult>;
@@ -37,3 +51,14 @@ export declare class DefaultPullSession<TResult> implements PullSession<TResult>
37
51
  private runKeepAlive;
38
52
  }
39
53
  export declare function newPullSession<TResult>(opts: PullSessionOptions<TResult>): DefaultPullSession<TResult>;
54
+ export declare class PullSearchSource<T, TResult, Options extends SearchOptions> {
55
+ private readonly options;
56
+ private session?;
57
+ private lifetime?;
58
+ constructor(options: {
59
+ start: (options: Options) => Promise<PullSession<TResult>>;
60
+ results: (result: TResult) => T[];
61
+ });
62
+ clear(): void;
63
+ search(opts: Options): Promise<SearchPage<T>>;
64
+ }
@@ -3,6 +3,31 @@
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 { notifyObserver } from "./events.js";
7
+ import { normalizeSearchPage, } from "./search.js";
8
+ // One account/context lifetime, including requests that have not returned a session yet.
9
+ export class PullSessionScope {
10
+ abort = new AbortController();
11
+ signal(signal) {
12
+ return signal
13
+ ? AbortSignal.any([this.abort.signal, signal])
14
+ : this.abort.signal;
15
+ }
16
+ reset() {
17
+ this.abort.abort();
18
+ this.abort = new AbortController();
19
+ }
20
+ }
21
+ export async function startPullSession(opts) {
22
+ opts.signal?.throwIfAborted();
23
+ const initial = await opts.pull_initial(opts.signal);
24
+ opts.signal?.throwIfAborted();
25
+ return newPullSession({
26
+ ...opts,
27
+ initial,
28
+ session_token: opts.resolve_session_token(initial),
29
+ });
30
+ }
6
31
  export class DefaultPullSession {
7
32
  initial;
8
33
  token;
@@ -16,7 +41,11 @@ export class DefaultPullSession {
16
41
  closedError;
17
42
  pullInProgressError;
18
43
  onError;
44
+ abort = new AbortController();
45
+ parentSignal;
46
+ onAbort = () => this.close();
19
47
  constructor(opts) {
48
+ opts.signal?.throwIfAborted();
20
49
  this.initial = opts.initial;
21
50
  this.token = opts.session_token;
22
51
  this.closed = false;
@@ -27,9 +56,11 @@ export class DefaultPullSession {
27
56
  this.closedError = opts.closed_error;
28
57
  this.pullInProgressError = opts.pull_in_progress_error;
29
58
  this.onError = opts.on_error;
59
+ this.parentSignal = opts.signal;
30
60
  this.timer = globalThis.setInterval(() => {
31
61
  void this.keepAlive();
32
62
  }, opts.keep_alive_interval_ms);
63
+ this.parentSignal?.addEventListener("abort", this.onAbort, { once: true });
33
64
  }
34
65
  get sessionToken() {
35
66
  return this.token;
@@ -50,12 +81,17 @@ export class DefaultPullSession {
50
81
  if (this.closed) {
51
82
  throw this.closedError();
52
83
  }
53
- const out = await this.pullNextFn(this.token);
54
- if (!this.closed) {
55
- this.token = this.resolveSessionToken(out);
56
- }
84
+ const out = await this.pullNextFn(this.token, this.abort.signal);
85
+ this.abort.signal.throwIfAborted();
86
+ this.token = this.resolveSessionToken(out);
57
87
  return out;
58
88
  }
89
+ catch (error) {
90
+ // A failed pull may already have advanced the server's seen-results set.
91
+ // Continuing this session could silently skip a page; refresh starts a new one.
92
+ this.close();
93
+ throw error;
94
+ }
59
95
  finally {
60
96
  this.pulling = false;
61
97
  }
@@ -66,6 +102,8 @@ export class DefaultPullSession {
66
102
  }
67
103
  this.closed = true;
68
104
  globalThis.clearInterval(this.timer);
105
+ this.parentSignal?.removeEventListener("abort", this.onAbort);
106
+ this.abort.abort();
69
107
  }
70
108
  async keepAlive() {
71
109
  if (this.closed || this.pulling || this.keepAliveTask !== undefined) {
@@ -84,14 +122,19 @@ export class DefaultPullSession {
84
122
  }
85
123
  async runKeepAlive() {
86
124
  try {
87
- const out = await this.keepAliveFn(this.token);
125
+ const out = await this.keepAliveFn(this.token, this.abort.signal);
88
126
  if (!this.closed) {
89
127
  this.token = this.resolveSessionToken(out);
90
128
  }
91
129
  }
92
130
  catch (err) {
93
131
  if (!this.closed) {
94
- this.onError?.(err);
132
+ this.close();
133
+ notifyObserver(() => {
134
+ if (this.onError)
135
+ return this.onError(err);
136
+ throw err;
137
+ });
95
138
  }
96
139
  }
97
140
  }
@@ -99,3 +142,62 @@ export class DefaultPullSession {
99
142
  export function newPullSession(opts) {
100
143
  return new DefaultPullSession(opts);
101
144
  }
145
+ // Adapts sequential pull sessions to the existing paged SearchController.
146
+ // Its clear method belongs to the controller's on_clear hook.
147
+ export class PullSearchSource {
148
+ options;
149
+ session;
150
+ lifetime;
151
+ constructor(options) {
152
+ this.options = options;
153
+ }
154
+ clear() {
155
+ this.lifetime?.abort();
156
+ this.lifetime = undefined;
157
+ this.session?.close();
158
+ this.session = undefined;
159
+ }
160
+ async search(opts) {
161
+ const page = normalizeSearchPage(opts.page);
162
+ if (page === normalizeSearchPage())
163
+ this.clear();
164
+ const lifetime = this.lifetime ?? new AbortController();
165
+ this.lifetime = lifetime;
166
+ const cancel = () => lifetime.abort();
167
+ opts.signal?.throwIfAborted();
168
+ opts.signal?.addEventListener("abort", cancel, { once: true });
169
+ try {
170
+ let out;
171
+ if (!this.session) {
172
+ const session = await this.options.start({
173
+ ...opts,
174
+ signal: lifetime.signal,
175
+ });
176
+ if (lifetime.signal.aborted || this.lifetime !== lifetime) {
177
+ session.close();
178
+ lifetime.signal.throwIfAborted();
179
+ throw new Error("Pull selection was replaced.");
180
+ }
181
+ this.session = session;
182
+ out = session.initial;
183
+ }
184
+ else {
185
+ out = await this.session.pullNext();
186
+ }
187
+ lifetime.signal.throwIfAborted();
188
+ const results = this.options.results(out);
189
+ const has_more = results.length > 0;
190
+ return {
191
+ results,
192
+ page,
193
+ per_page: results.length,
194
+ has_more,
195
+ next_page: has_more ? page + 1 : page,
196
+ done: !has_more,
197
+ };
198
+ }
199
+ finally {
200
+ opts.signal?.removeEventListener("abort", cancel);
201
+ }
202
+ }
203
+ }
@@ -12,6 +12,7 @@ export type SearchControllerOptions<T, Options extends SearchOptions> = {
12
12
  search: (opts: Options) => Promise<SearchPage<T>>;
13
13
  on_change: (state: SearchState<T>) => void;
14
14
  on_error?: (error: unknown) => void;
15
+ on_clear?: () => void;
15
16
  };
16
17
  export declare class SearchController<T extends SearchResultWithID, Options extends SearchOptions> {
17
18
  private readonly options;
@@ -24,6 +24,7 @@ export class SearchController {
24
24
  clear() {
25
25
  this.request?.abort();
26
26
  this.request = undefined;
27
+ this.options.on_clear?.();
27
28
  this.selection = undefined;
28
29
  this.next_page = normalizeSearchPage();
29
30
  this.publish(this.emptyState());
@@ -1,8 +1,33 @@
1
1
  import { type FeedFavorite, type FeedObject, type FeedPost, type FeedReaction, type OperationResult, type RPFeedPull } from "@fabriktor/schema";
2
- import type { FeedOperator, OperationResultOf, StorageUploadFile } from "@fabriktor/client";
2
+ import { type FeedOperator, type OperationResultOf, type StorageUploadFile } from "@fabriktor/client";
3
3
  import { type PullSession, type PullSessionErrorHandler } from "../core/pull.js";
4
- import { type SearchOptions, type SearchPage } from "../core/search.js";
4
+ import { type OwnerSearchOptions, type SearchPage } from "../core/search.js";
5
5
  import { type StorageFXOperator, type StoragePersonalBoxUploadResult } from "../storage/storage.js";
6
+ import { EventBus } from "../core/events.js";
7
+ import type { MultiCreateCheckpoint } from "../core/multi-create.js";
8
+ export * from "./validation.js";
9
+ export * from "./media.js";
10
+ import { type SaveFeedPostOptions, type FeedPostSaveResult, type FeedPostSaveState } from "./media.js";
11
+ export declare const FeedEvents: {
12
+ Changed: import("../index.js").EventKey<{
13
+ post_id: string;
14
+ }>;
15
+ };
16
+ export type FeedReadOptions = {
17
+ id: string;
18
+ signal?: AbortSignal;
19
+ };
20
+ export type SavedFeedPost = FeedFavorite & {
21
+ post: FeedPost | null;
22
+ };
23
+ export type FeedFavoriteListOptions = {
24
+ owner_id: string;
25
+ signal?: AbortSignal;
26
+ };
27
+ export type DeleteFeedPostOptions = {
28
+ record: FeedPost;
29
+ owner_id: string;
30
+ };
6
31
  type FeedPullOptions = Parameters<FeedOperator["pull"]>[0];
7
32
  type FeedPostInsertOptions = Parameters<FeedOperator["insertOneFeedPost"]>[0];
8
33
  type FeedPostReplaceOptions = Parameters<FeedOperator["replaceOneFeedPost"]>[0];
@@ -30,13 +55,15 @@ export declare const FeedMediaPostStatus: {
30
55
  };
31
56
  export type FeedMediaPostStatus = (typeof FeedMediaPostStatus)[keyof typeof FeedMediaPostStatus];
32
57
  export type FeedReactionType = FeedReactionInsert["reaction_type"];
33
- export type CreateFeedPostOptions = FeedPostInsertOptions;
34
- export type ReplaceFeedPostOptions = FeedPostReplaceOptions;
58
+ export type CreateFeedPostOptions = FeedPostInsertOptions & MultiCreateCheckpoint;
59
+ export type ReplaceFeedPostOptions = FeedPostReplaceOptions & {
60
+ expected?: Pick<FeedPost, "owner_id" | "updated_at">;
61
+ };
35
62
  export type CreateFeedReactionOptions = FeedReactionInsertOptions;
36
63
  export type ReplaceFeedReactionOptions = FeedReactionReplaceOptions;
37
64
  export type CreateFeedFavoriteOptions = FeedFavoriteInsertOptions;
38
65
  export type ReplaceFeedFavoriteOptions = FeedFavoriteReplaceOptions;
39
- export type SearchFeedPostsOptions = SearchOptions;
66
+ export type SearchFeedPostsOptions = OwnerSearchOptions;
40
67
  export type SearchFeedPostsPage = SearchPage<FeedPost>;
41
68
  export type FeedPullContext = Pick<FeedPullOptions, "contact_ids" | "pull_type" | "entity_id" | "research">;
42
69
  export type FeedTextPostContent = Pick<FeedPostInsert, "text">;
@@ -45,11 +72,13 @@ export type FeedPullResult = OperationResultOf<RPFeedPull>;
45
72
  export type FeedSession = PullSession<FeedPullResult>;
46
73
  export type FeedSessionErrorHandler = PullSessionErrorHandler;
47
74
  export type FeedFXOptions = {
75
+ events?: EventBus;
48
76
  feed: FeedOperator;
49
77
  storage_fx: StorageFXOperator;
50
78
  keep_alive_interval_ms: number;
51
79
  };
52
80
  export type StartFeedSessionOptions = FeedPullContext & {
81
+ signal?: AbortSignal;
53
82
  on_error?: FeedSessionErrorHandler;
54
83
  };
55
84
  export type AddFeedReactionOptions = {
@@ -79,7 +108,10 @@ export type CreateFeedReferenceOptions = {
79
108
  export type RemoveFeedReferenceOptions = {
80
109
  post: FeedPost;
81
110
  };
82
- export type FeedMediaUploadOptions = {
111
+ export type FeedMediaUploadOptions = MultiCreateCheckpoint & {
112
+ state?: FeedPostSaveState;
113
+ signal?: AbortSignal;
114
+ idempotency_key?: string;
83
115
  user_id?: string;
84
116
  uploader_id?: string;
85
117
  full_quality?: boolean;
@@ -132,6 +164,19 @@ export type CreateFeedPostWithMediaResult = {
132
164
  error: unknown;
133
165
  };
134
166
  export interface FeedFXOperator {
167
+ saveFeedPost(opts: SaveFeedPostOptions): Promise<FeedPostSaveResult>;
168
+ readOwnReaction(opts: FeedReadOptions): Promise<FeedReaction | null>;
169
+ readFeedPost(opts: FeedReadOptions): Promise<FeedPost | null>;
170
+ listSavedFeedPosts(opts: FeedFavoriteListOptions): Promise<SavedFeedPost[]>;
171
+ removeSavedFeedPost(opts: {
172
+ id: string;
173
+ owner_id: string;
174
+ }): Promise<void>;
175
+ deleteFeedPost(opts: DeleteFeedPostOptions): Promise<void>;
176
+ restrictFeed(opts: {
177
+ new_visibility: number;
178
+ }): Promise<void>;
179
+ reset(): void;
135
180
  createFeedPost(opts: CreateFeedPostOptions): Promise<FeedPost>;
136
181
  replaceFeedPost(opts: ReplaceFeedPostOptions): Promise<FeedPost>;
137
182
  searchFeedPosts(opts: SearchFeedPostsOptions): Promise<SearchFeedPostsPage>;
@@ -153,6 +198,9 @@ export interface FeedFXOperator {
153
198
  createReferenceWithMedia(opts: CreateFeedReferenceWithMediaOptions): Promise<CreateFeedPostWithMediaResult>;
154
199
  }
155
200
  export declare class FeedFX implements FeedFXOperator {
201
+ private readonly sessions;
202
+ private readonly events;
203
+ private mutating;
156
204
  private feed;
157
205
  private storage_fx;
158
206
  private keep_alive_interval_ms;
@@ -161,9 +209,25 @@ export declare class FeedFX implements FeedFXOperator {
161
209
  private removed_actions;
162
210
  private action_ids;
163
211
  constructor(opts: FeedFXOptions);
212
+ reset(): void;
164
213
  createFeedPost(opts: CreateFeedPostOptions): Promise<FeedPost>;
165
214
  replaceFeedPost(opts: ReplaceFeedPostOptions): Promise<FeedPost>;
166
215
  searchFeedPosts(opts: SearchFeedPostsOptions): Promise<SearchFeedPostsPage>;
216
+ readOwnReaction(opts: FeedReadOptions): Promise<FeedReaction | null>;
217
+ readFeedPost(opts: FeedReadOptions): Promise<FeedPost | null>;
218
+ listSavedFeedPosts(opts: FeedFavoriteListOptions): Promise<SavedFeedPost[]>;
219
+ removeSavedFeedPost(opts: {
220
+ id: string;
221
+ owner_id: string;
222
+ }): Promise<void>;
223
+ deleteFeedPost(opts: DeleteFeedPostOptions): Promise<void>;
224
+ restrictFeed(opts: {
225
+ new_visibility: number;
226
+ }): Promise<void>;
227
+ private checkVersion;
228
+ private requireFeedPost;
229
+ private changed;
230
+ private mutate;
167
231
  createFeedReaction(opts: CreateFeedReactionOptions): Promise<FeedReaction>;
168
232
  replaceFeedReaction(opts: ReplaceFeedReactionOptions): Promise<FeedReaction>;
169
233
  createFeedFavorite(opts: CreateFeedFavoriteOptions): Promise<FeedFavorite>;
@@ -177,6 +241,7 @@ export declare class FeedFX implements FeedFXOperator {
177
241
  removeReply(opts: RemoveFeedReplyOptions): Promise<OperationResult>;
178
242
  createReference(opts: CreateFeedReferenceOptions): Promise<OperationResult>;
179
243
  removeReference(opts: RemoveFeedReferenceOptions): Promise<OperationResult>;
244
+ saveFeedPost(opts: SaveFeedPostOptions): Promise<FeedPostSaveResult>;
180
245
  createPostWithMedia(opts: CreateFeedPostWithMediaOptions): Promise<CreateFeedPostWithMediaResult>;
181
246
  createReplyWithMedia(opts: CreateFeedReplyWithMediaOptions): Promise<CreateFeedPostWithMediaResult>;
182
247
  createReferenceWithMedia(opts: CreateFeedReferenceWithMediaOptions): Promise<CreateFeedPostWithMediaResult>;
@@ -196,4 +261,3 @@ export declare class FeedFX implements FeedFXOperator {
196
261
  private setActionRunning;
197
262
  }
198
263
  export declare function newFeedFX(opts: FeedFXOptions): FeedFX;
199
- export {};