@fabriktor/fx 0.0.147 → 0.0.149

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.
@@ -1,5 +1,5 @@
1
1
  import { normalizeIDs, resolvedID } from "../core/id.js";
2
- import { isFabriktorUser, userSupportsMode } from "../users/users.js";
2
+ import { isFabriktorUser, isOtherUserID, userSupportsMode, } from "../users/users.js";
3
3
  // Recipients are all other members of the conversation, resolved before use.
4
4
  // Sender modes apply now; recipient modes describe what their identity supports.
5
5
  // Do not depend on the recipient's current mode, which Public() omits.
@@ -10,7 +10,7 @@ export function canSendChatForm(policy, actor, recipients) {
10
10
  (policy.allow_group || recipients.length === 1) &&
11
11
  normalizeIDs(recipients.map((recipient) => recipient.id)).length ===
12
12
  recipients.length &&
13
- recipients.every((recipient) => recipient.id !== actor.owner_id &&
13
+ recipients.every((recipient) => isOtherUserID(recipient.id, actor.owner_id) &&
14
14
  isFabriktorUser(recipient) &&
15
15
  policy.recipient_modes.some((mode) => userSupportsMode(recipient, mode))));
16
16
  }
@@ -48,6 +48,7 @@ export type ChatSendQueueOptions = {
48
48
  upload: (entry: QueuedChat, signal: AbortSignal) => Promise<boolean>;
49
49
  changed?: (chat?: Chat) => void;
50
50
  online?: () => boolean;
51
+ on_error?: (error: unknown) => void;
51
52
  };
52
53
  export declare class ChatSendQueue {
53
54
  private readonly opts;
@@ -87,6 +88,7 @@ export declare class ChatSendQueue {
87
88
  private startConfirmation;
88
89
  private startUpload;
89
90
  private recoverUpload;
91
+ private report;
90
92
  private waitForRetry;
91
93
  private recoverStorage;
92
94
  private scheduleRecovery;
@@ -4,7 +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
  import { requiredOperationID } from "../core/id.js";
7
- import { createEvent, EventBus } from "../core/events.js";
7
+ import { createEvent, EventBus, notifyObserver, } from "../core/events.js";
8
8
  import { PersistentQueue, QueueWakeup, nextQueueRetry, } from "../core/queue.js";
9
9
  import { newOptimisticChat } from "./optimistic.js";
10
10
  export const ChatSendStatus = {
@@ -141,6 +141,7 @@ export class ChatSendQueue {
141
141
  this.pendingSaves.set(entry.id, failed);
142
142
  this.replace(failed);
143
143
  if (entry.owner_id === this.owner) {
144
+ this.report(err);
144
145
  this.error = err;
145
146
  this.publish();
146
147
  }
@@ -198,7 +199,9 @@ export class ChatSendQueue {
198
199
  }
199
200
  for (const entry of stored) {
200
201
  if (chatSendCanConfirm(entry) ||
201
- (!entry.chat && entry.server_id && this.confirmed.has(entry.server_id))) {
202
+ (!entry.chat &&
203
+ entry.server_id &&
204
+ this.confirmed.has(entry.server_id))) {
202
205
  this.startConfirmation(entry, owner, signal);
203
206
  }
204
207
  if (entry.chat && entry.status === ChatSendStatus.Uploading) {
@@ -208,6 +211,7 @@ export class ChatSendQueue {
208
211
  }
209
212
  catch (err) {
210
213
  if (this.current(owner, signal)) {
214
+ this.report(err);
211
215
  this.error = err;
212
216
  this.publish();
213
217
  this.recoverStorage();
@@ -321,6 +325,7 @@ export class ChatSendQueue {
321
325
  select: (entry) => entry.input.chat_room_id === room && chatSendCanCreate(entry),
322
326
  on_error: (err) => {
323
327
  if (this.current(owner, signal)) {
328
+ this.report(err);
324
329
  this.error = err;
325
330
  this.publish();
326
331
  this.recoverStorage();
@@ -390,12 +395,14 @@ export class ChatSendQueue {
390
395
  }
391
396
  this.startConfirmation(current, owner, signal);
392
397
  }
393
- catch {
398
+ catch (error) {
394
399
  // remove() owns deliberate cancellation and removes the durable entry
395
400
  // under this same room lock. Do not publish a transient send failure.
396
401
  if (this.cancellations.has(entry.id) && !current.server_id) {
397
402
  return false;
398
403
  }
404
+ if (this.current(owner, signal))
405
+ this.report(error);
399
406
  current = {
400
407
  ...current,
401
408
  attempted: current.attempted && (requestStarted || !!current.server_id),
@@ -409,6 +416,7 @@ export class ChatSendQueue {
409
416
  this.pendingCreates.delete(current.id);
410
417
  }
411
418
  catch (storageError) {
419
+ this.report(storageError);
412
420
  this.error = storageError;
413
421
  this.publish();
414
422
  this.recoverStorage();
@@ -487,7 +495,9 @@ export class ChatSendQueue {
487
495
  this.opts.changed?.(chat);
488
496
  }
489
497
  }
490
- catch {
498
+ catch (error) {
499
+ if (this.current(owner, signal))
500
+ this.report(error);
491
501
  await this.save({
492
502
  ...current,
493
503
  retry: undefined,
@@ -501,6 +511,7 @@ export class ChatSendQueue {
501
511
  })
502
512
  .catch((err) => {
503
513
  if (this.current(owner, signal)) {
514
+ this.report(err);
504
515
  this.error = err;
505
516
  this.publish();
506
517
  this.recoverStorage();
@@ -563,7 +574,9 @@ export class ChatSendQueue {
563
574
  this.publish();
564
575
  this.opts.changed?.(this.confirmed.get(entry.server_id));
565
576
  }
566
- catch {
577
+ catch (error) {
578
+ if (this.current(owner, signal))
579
+ this.report(error);
567
580
  await this.save({
568
581
  ...entry,
569
582
  status: ChatSendStatus.UploadInterrupted,
@@ -572,6 +585,7 @@ export class ChatSendQueue {
572
585
  })
573
586
  .catch((err) => {
574
587
  if (this.current(owner, signal)) {
588
+ this.report(err);
575
589
  this.error = err;
576
590
  this.publish();
577
591
  }
@@ -616,6 +630,7 @@ export class ChatSendQueue {
616
630
  if (!this.current(owner, signal)) {
617
631
  return;
618
632
  }
633
+ this.report(err);
619
634
  this.error = err;
620
635
  }
621
636
  await this.save({
@@ -625,12 +640,17 @@ export class ChatSendQueue {
625
640
  })
626
641
  .catch((err) => {
627
642
  if (this.current(owner, signal)) {
643
+ this.report(err);
628
644
  this.error = err;
629
645
  this.publish();
630
646
  }
631
647
  })
632
648
  .finally(() => this.uploads.delete(entry.id));
633
649
  }
650
+ report(error) {
651
+ if (this.opts.on_error)
652
+ notifyObserver(() => this.opts.on_error?.(error));
653
+ }
634
654
  waitForRetry(entry) {
635
655
  if (!entry.retry || entry.retry.after <= Date.now())
636
656
  return false;
@@ -5,6 +5,8 @@ export type EventKey<T> = symbol & {
5
5
  export type EventListener<T> = (value: T) => void | Promise<void>;
6
6
  export declare function createEvent<T>(): EventKey<T>;
7
7
  export declare function notifyObserver(run: () => unknown): void;
8
+ type ObserverErrorHandler = (error: unknown) => void;
9
+ export declare function setObserverErrorHandler(handler: ObserverErrorHandler): () => void;
8
10
  export declare class EventBus {
9
11
  private readonly listeners;
10
12
  subscribe<T>(event: EventKey<T>, listener: EventListener<NoInfer<T>>, initial?: () => T): () => void;
@@ -14,9 +14,28 @@ export function notifyObserver(run) {
14
14
  observerError(err);
15
15
  }
16
16
  }
17
- function observerError(err) {
17
+ function defaultObserverError(err) {
18
18
  console.error("Event observer failed.", err);
19
19
  }
20
+ let observerErrorHandler = defaultObserverError;
21
+ // Apps own diagnostic presentation; independent FX consumers keep the existing
22
+ // console fallback. Observer failures must never break a completed mutation.
23
+ export function setObserverErrorHandler(handler) {
24
+ const previous = observerErrorHandler;
25
+ observerErrorHandler = handler;
26
+ return () => {
27
+ if (observerErrorHandler === handler)
28
+ observerErrorHandler = previous;
29
+ };
30
+ }
31
+ function observerError(err) {
32
+ try {
33
+ observerErrorHandler(err);
34
+ }
35
+ catch {
36
+ defaultObserverError(err);
37
+ }
38
+ }
20
39
  export class EventBus {
21
40
  listeners = new Map();
22
41
  subscribe(event, listener, initial) {
@@ -11,6 +11,7 @@ export type SearchState<T> = {
11
11
  export type SearchControllerOptions<T, Options extends SearchOptions> = {
12
12
  search: (opts: Options) => Promise<SearchPage<T>>;
13
13
  on_change: (state: SearchState<T>) => void;
14
+ on_error?: (error: unknown) => void;
14
15
  };
15
16
  export declare class SearchController<T extends SearchResultWithID, Options extends SearchOptions> {
16
17
  private readonly options;
@@ -5,6 +5,7 @@
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
6
  import { appendUniqueSearchResults, normalizeSearchPage, } from "./search.js";
7
7
  import { errUnknown } from "./err.js";
8
+ import { notifyObserver } from "./events.js";
8
9
  // Holds only the current selection and its displayed pages. Previous queries
9
10
  // are discarded, and revisiting a selection always makes a backend request.
10
11
  export class SearchController {
@@ -96,6 +97,8 @@ export class SearchController {
96
97
  loading_more: false,
97
98
  error: errUnknown(error),
98
99
  });
100
+ if (this.options.on_error)
101
+ notifyObserver(() => this.options.on_error?.(error));
99
102
  return false;
100
103
  }
101
104
  finally {
@@ -17,6 +17,7 @@ export type UpdateExternalUserOptions = {
17
17
  };
18
18
  export declare function userSupportsMode(user: Pick<User, "account_identity"> | null | undefined, mode: User["active_mode"]): boolean;
19
19
  export declare function isFabriktorUser(user: Pick<User, "account_identity"> | null | undefined): boolean;
20
+ export declare function isOtherUserID(user_id: string | null | undefined, owner_id: string | null | undefined): boolean;
20
21
  export declare function canManageExternalUser(user: User | null | undefined, owner_id: string): boolean;
21
22
  export type SearchExternalUsersOptions = SearchUsersOptions;
22
23
  export type SearchExternalUsersPage = SearchUsersPage;
@@ -44,6 +44,13 @@ export function userSupportsMode(user, mode) {
44
44
  export function isFabriktorUser(user) {
45
45
  return userSupportsMode(user, UserTypeClient);
46
46
  }
47
+ // Client capability and being a different party are separate requirements.
48
+ // Apply this to counterparties, not to ownership or employee scope filters.
49
+ export function isOtherUserID(user_id, owner_id) {
50
+ const userID = resolvedID(user_id ?? undefined);
51
+ const ownerID = resolvedID(owner_id ?? undefined);
52
+ return !!userID && !!ownerID && userID !== ownerID;
53
+ }
47
54
  export function canManageExternalUser(user, owner_id) {
48
55
  return (!!resolvedID(owner_id) &&
49
56
  user?.account_identity === UserTypeExternal &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.147",
3
+ "version": "0.0.149",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {