@fabriktor/fx 0.0.148 → 0.0.150

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.
@@ -14,19 +14,24 @@ export function newChatData(type, payload) {
14
14
  type,
15
15
  payload,
16
16
  };
17
- if (!validChatData(data))
18
- throw errValidation({ msg: msgChatData });
19
- return structuredClone(data);
17
+ // JSON snapshots also accept reactive UI objects without structuredClone's
18
+ // Proxy failure. Validation and size limits are shared with sending.
19
+ return JSON.parse(serializeChatData(data));
20
20
  }
21
21
  export function encodeChatContent(text, data) {
22
22
  if (!data)
23
23
  return text;
24
- if (!text.trim() || text.includes(chatDataMarker) || !validChatData(data))
24
+ if (!text.trim() || text.includes(chatDataMarker))
25
+ throw errValidation({ msg: msgChatData });
26
+ return text.trim() + chatDataMarker + serializeChatData(data);
27
+ }
28
+ function serializeChatData(data) {
29
+ if (!validChatData(data))
25
30
  throw errValidation({ msg: msgChatData });
26
31
  const value = JSON.stringify(data);
27
32
  if (new TextEncoder().encode(value).length > chatDataMaximumBytes)
28
33
  throw errValidation({ msg: msgChatData });
29
- return text.trim() + chatDataMarker + value;
34
+ return value;
30
35
  }
31
36
  export function decodeChatContent(value) {
32
37
  const boundary = value.indexOf(chatDataMarker);
@@ -1,9 +1,16 @@
1
1
  import type { User } from "@fabriktor/schema";
2
2
  import type { EmploymentActor } from "../contacts/scope.js";
3
+ import { type ChatData, type ChatJSON } from "./data.js";
3
4
  export type ChatFormRecipient = Pick<User, "id" | "account_identity">;
4
5
  export type ChatFormPolicy = {
5
6
  readonly sender_modes: readonly User["active_mode"][];
6
7
  readonly recipient_modes: readonly User["active_mode"][];
7
8
  readonly allow_group: boolean;
8
9
  };
10
+ export type ChatFormContextDefinition<T extends ChatJSON> = {
11
+ readonly version: number;
12
+ readonly read: (value: ChatJSON | undefined) => T | undefined;
13
+ };
14
+ export declare function newChatFormData<T extends ChatJSON>(type: string, payload: ChatData["payload"], definition: ChatFormContextDefinition<T>, context: T): ChatData;
15
+ export declare function readChatFormContext<T extends ChatJSON>(data: ChatData, definition: ChatFormContextDefinition<T>): T;
9
16
  export declare function canSendChatForm(policy: ChatFormPolicy, actor: EmploymentActor, recipients: readonly ChatFormRecipient[]): boolean;
@@ -1,5 +1,29 @@
1
+ import { errValidation } from "../core/err.js";
1
2
  import { normalizeIDs, resolvedID } from "../core/id.js";
3
+ import { newChatData } from "./data.js";
2
4
  import { isFabriktorUser, isOtherUserID, userSupportsMode, } from "../users/users.js";
5
+ const msgFormContext = "The form details are missing or unsupported. Ask the sender to send a new form.";
6
+ export function newChatFormData(type, payload, definition, context) {
7
+ const value = definition.read(context);
8
+ if (value === undefined)
9
+ throw errValidation({ msg: msgFormContext });
10
+ return newChatData(type, {
11
+ ...payload,
12
+ form_context: { version: definition.version, data: value },
13
+ });
14
+ }
15
+ export function readChatFormContext(data, definition) {
16
+ const context = data.payload.form_context;
17
+ if (context &&
18
+ typeof context === "object" &&
19
+ !Array.isArray(context) &&
20
+ context.version === definition.version) {
21
+ const value = definition.read(context.data);
22
+ if (value !== undefined)
23
+ return value;
24
+ }
25
+ throw errValidation({ msg: msgFormContext });
26
+ }
3
27
  // Recipients are all other members of the conversation, resolved before use.
4
28
  // Sender modes apply now; recipient modes describe what their identity supports.
5
29
  // Do not depend on the recipient's current mode, which Public() omits.
@@ -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 {
@@ -12,6 +12,7 @@ import { runPreActionCheck } from "../core/pre-action.js";
12
12
  import { runOwnerSearch, runSearch, withOwnerQuery, } from "../core/search.js";
13
13
  const msgJobIDRequired = "Job ID is required.";
14
14
  const msgLocationIDRequired = "Location ID is required.";
15
+ const msgLocationClientRequired = "Choose a client for this location.";
15
16
  const msgOwnerIDRequired = "Owner ID is required.";
16
17
  const msgMemberIDRequired = "Member ID is required.";
17
18
  const msgJobLocationMismatch = "Job location does not match the provided parent location.";
@@ -59,9 +60,11 @@ export class JobsFX {
59
60
  this.now = opts.now ?? (() => new globalThis.Date());
60
61
  }
61
62
  async createLocation(opts) {
63
+ const client_id = requiredID(opts.in.client_id, "client_id", msgLocationClientRequired);
62
64
  const out = await this.jobs.insertOneLocation({
63
65
  in: {
64
66
  ...opts.in,
67
+ client_id,
65
68
  is_archived: false,
66
69
  current_members: normalizeIDs(opts.in.current_members),
67
70
  },
@@ -72,8 +75,12 @@ export class JobsFX {
72
75
  async updateLocation(opts) {
73
76
  const owner_id = requiredID(opts.owner_id, "owner_id", msgOwnerIDRequired);
74
77
  const id = requiredID(opts.location.id, "location", msgLocationIDRequired);
78
+ const client_id = requiredID(opts.in.client_id === undefined
79
+ ? opts.location.client_id
80
+ : opts.in.client_id, "client_id", msgLocationClientRequired);
75
81
  const input = {
76
82
  ...opts.in,
83
+ client_id,
77
84
  ...locationAddressInput(opts.location),
78
85
  };
79
86
  if (opts.in.current_members !== undefined) {
@@ -2,7 +2,7 @@ import type { JobsOperator, RoutesOperator, UsersOperator } from "@fabriktor/cli
2
2
  import { type ClientAvailability, type InClientAvailability, type Location, type PrefCompany, type User } from "@fabriktor/schema";
3
3
  import type { PreferencesFXOperator } from "../preferences/preferences.js";
4
4
  import { type ChatData } from "../chat/data.js";
5
- import { type ChatFormRecipient } from "../chat/forms.js";
5
+ import { type ChatFormContextDefinition, type ChatFormRecipient } from "../chat/forms.js";
6
6
  import { type RoutesActor } from "./workspace.js";
7
7
  export declare const clientAvailabilityMinimumWindows = 3;
8
8
  export declare const availabilityRequestPolicy: {
@@ -21,23 +21,35 @@ export type AvailabilityContext = {
21
21
  location: Location;
22
22
  employer: User;
23
23
  client: User;
24
- company: PrefCompany;
24
+ working_hours?: AvailabilityWorkingHours;
25
25
  timezone: string;
26
26
  };
27
27
  export type AvailabilityReaders = {
28
28
  jobs: Pick<JobsOperator, "findOneLocation">;
29
29
  users: Pick<UsersOperator, "findOneUser">;
30
- preferences: Pick<PreferencesFXOperator, "getPrefCompany">;
30
+ };
31
+ export type AvailabilityWorkingHours = Pick<PrefCompany, "work_day_start_hour" | "work_day_end_hour">;
32
+ export type AvailabilityFormContext = {
33
+ working_hours: AvailabilityWorkingHours;
34
+ timezone: Location["address_timezone"];
35
+ };
36
+ export declare const availabilityFormContextDefinition: ChatFormContextDefinition<AvailabilityFormContext>;
37
+ export type AvailabilityContextOptions = {
38
+ request?: ChatData;
39
+ signal?: AbortSignal;
31
40
  };
32
41
  export declare function availabilityReference(data: ChatData): AvailabilityReference | undefined;
33
42
  export declare function availabilityRequest(location: Location, actor: RoutesActor): ChatData;
34
43
  export declare function canSendAvailabilityRequest(data: ChatData, actor: RoutesActor, recipients: readonly ChatFormRecipient[]): boolean;
44
+ export declare function loadAvailabilityRequestContext(readers: AvailabilityReaders & {
45
+ preferences: Pick<PreferencesFXOperator, "getPrefCompany">;
46
+ }, actor: RoutesActor, data: ChatData): Promise<AvailabilityFormContext>;
35
47
  export declare function availabilitySubmission(reference: AvailabilityReference, request_id: string, ids: readonly string[]): ChatData;
36
- export declare function loadAvailabilityContext(readers: AvailabilityReaders, actor: RoutesActor, reference: AvailabilityReference, signal?: AbortSignal): Promise<AvailabilityContext>;
48
+ export declare function loadAvailabilityContext(readers: AvailabilityReaders, actor: RoutesActor, reference: AvailabilityReference, { request, signal }?: AvailabilityContextOptions): Promise<AvailabilityContext>;
37
49
  export declare function validateAvailabilityWindows(inputs: readonly InClientAvailability[], context: AvailabilityContext, minimum?: number): void;
38
50
  export declare function loadAvailabilitySelection(readers: AvailabilityReaders & {
39
51
  routes: Pick<RoutesOperator, "findOneClientAvailability">;
40
- }, actor: RoutesActor, reference: AvailabilityReference, ids: readonly string[], signal?: AbortSignal): Promise<{
52
+ }, actor: RoutesActor, reference: AvailabilityReference, ids: readonly string[], options?: AvailabilityContextOptions): Promise<{
41
53
  context: AvailabilityContext;
42
54
  windows: ClientAvailability[];
43
55
  }>;
@@ -5,7 +5,7 @@ import { hoursPerDay, minutesOfDay, minutesPerHour, sameCalendarDay, } from "../
5
5
  import { errValidation } from "../core/err.js";
6
6
  import { normalizeIDs, resolvedID } from "../core/id.js";
7
7
  import { newChatData } from "../chat/data.js";
8
- import { canSendChatForm, } from "../chat/forms.js";
8
+ import { canSendChatForm, readChatFormContext, } from "../chat/forms.js";
9
9
  import { routeCanPlan } from "./workspace.js";
10
10
  import { validateClientAvailabilityInput } from "./validation.js";
11
11
  export const clientAvailabilityMinimumWindows = 3;
@@ -18,10 +18,35 @@ export const RoutesChatDataType = {
18
18
  AvailabilityRequest: "routes.availability.request",
19
19
  AvailabilitySubmitted: "routes.availability.submitted",
20
20
  };
21
+ export const availabilityFormContextDefinition = {
22
+ version: 1,
23
+ read: (value) => {
24
+ if (!value || typeof value !== "object" || Array.isArray(value))
25
+ return;
26
+ const { working_hours, timezone } = value;
27
+ if (!working_hours ||
28
+ typeof working_hours !== "object" ||
29
+ Array.isArray(working_hours) ||
30
+ typeof timezone !== "string" ||
31
+ !timezone.trim())
32
+ return;
33
+ const { work_day_start_hour: start, work_day_end_hour: end } = working_hours;
34
+ if (typeof start !== "number" ||
35
+ typeof end !== "number" ||
36
+ ![start, end].every((hour) => Number.isInteger(hour) && hour >= 0 && hour < hoursPerDay) ||
37
+ start >= end)
38
+ return;
39
+ return {
40
+ timezone,
41
+ working_hours: { work_day_start_hour: start, work_day_end_hour: end },
42
+ };
43
+ },
44
+ };
21
45
  const msgAvailabilityAccess = "This availability does not match the client and employer of the location.";
22
46
  const msgAvailabilityTimezone = "The location needs a valid saved address timezone.";
47
+ const msgAvailabilityTimezoneChanged = "The location timezone has changed. Ask the sender to send a new form.";
23
48
  const msgAvailabilityHours = "The employer's working hours must form a valid same-day window.";
24
- const msgAvailabilityWindows = "Provide enough distinct, non-overlapping availability windows within the employer's working hours.";
49
+ const msgAvailabilityWindows = "Provide enough distinct, non-overlapping availability windows and respect the hours supplied with the request.";
25
50
  const msgAvailabilityClient = "Visit times can only be requested from clients with a Fabriktor account. Arrange external contacts’ visit times directly.";
26
51
  const msgAvailabilityData = "The availability request is invalid.";
27
52
  export function availabilityReference(data) {
@@ -69,6 +94,30 @@ export function canSendAvailabilityRequest(data, actor, recipients) {
69
94
  canSendChatForm(availabilityRequestPolicy, actor, recipients) &&
70
95
  reference.client_id === recipients[0].id);
71
96
  }
97
+ // Registry pre-action: only the employer preparing the request reads their
98
+ // preferences. Transfer the two hours, never the complete preferences record.
99
+ export async function loadAvailabilityRequestContext(readers, actor, data) {
100
+ const reference = availabilityReference(data);
101
+ if (data.type !== RoutesChatDataType.AvailabilityRequest ||
102
+ !reference ||
103
+ !routeCanPlan(actor) ||
104
+ reference.employer_id !== actor.owner_id)
105
+ throw errValidation({ msg: msgAvailabilityAccess });
106
+ const [context, company] = await Promise.all([
107
+ loadAvailabilityContext(readers, actor, reference),
108
+ readers.preferences.getPrefCompany({ owner_id: actor.owner_id }),
109
+ ]);
110
+ const supplied = availabilityFormContextDefinition.read({
111
+ timezone: context.timezone,
112
+ working_hours: {
113
+ work_day_start_hour: company.work_day_start_hour,
114
+ work_day_end_hour: company.work_day_end_hour,
115
+ },
116
+ });
117
+ if (!supplied)
118
+ throw errValidation({ msg: msgAvailabilityHours });
119
+ return supplied;
120
+ }
72
121
  export function availabilitySubmission(reference, request_id, ids) {
73
122
  const data = newChatData(RoutesChatDataType.AvailabilitySubmitted, {
74
123
  ...reference,
@@ -79,7 +128,19 @@ export function availabilitySubmission(reference, request_id, ids) {
79
128
  throw errValidation({ msg: msgAvailabilityData });
80
129
  return data;
81
130
  }
82
- export async function loadAvailabilityContext(readers, actor, reference, signal) {
131
+ export async function loadAvailabilityContext(readers, actor, reference, { request, signal } = {}) {
132
+ const supplied = request
133
+ ? readChatFormContext(request, availabilityFormContextDefinition)
134
+ : undefined;
135
+ if (request) {
136
+ const requestReference = availabilityReference(request);
137
+ if (request.type !== RoutesChatDataType.AvailabilityRequest ||
138
+ !requestReference ||
139
+ requestReference.employer_id !== reference.employer_id ||
140
+ requestReference.client_id !== reference.client_id ||
141
+ requestReference.location_id !== reference.location_id)
142
+ throw errValidation({ msg: msgAvailabilityAccess });
143
+ }
83
144
  const location = (await readers.jobs.findOneLocation({ id: reference.location_id, signal })).value;
84
145
  if (!location ||
85
146
  location.id !== reference.location_id ||
@@ -90,10 +151,9 @@ export async function loadAvailabilityContext(readers, actor, reference, signal)
90
151
  ? actor.owner_id !== reference.client_id
91
152
  : !routeCanPlan(actor) || actor.owner_id !== reference.employer_id))
92
153
  throw errValidation({ msg: msgAvailabilityAccess });
93
- const [result, clientResult, company] = await Promise.all([
154
+ const [result, clientResult] = await Promise.all([
94
155
  readers.users.findOneUser({ id: reference.employer_id, signal }),
95
156
  readers.users.findOneUser({ id: reference.client_id, signal }),
96
- readers.preferences.getPrefCompany({ owner_id: reference.employer_id }),
97
157
  ]);
98
158
  const employer = result.value;
99
159
  if (!employer ||
@@ -108,11 +168,15 @@ export async function loadAvailabilityContext(readers, actor, reference, signal)
108
168
  const timezone = addressTimezone(location);
109
169
  if (!timezone)
110
170
  throw errValidation({ msg: msgAvailabilityTimezone });
111
- const { work_day_start_hour: start, work_day_end_hour: end } = company;
112
- if (![start, end].every((hour) => Number.isInteger(hour) && hour >= 0 && hour < hoursPerDay) ||
113
- start >= end)
114
- throw errValidation({ msg: msgAvailabilityHours });
115
- return { location, employer, client, company, timezone };
171
+ if (supplied && supplied.timezone !== timezone)
172
+ throw errValidation({ msg: msgAvailabilityTimezoneChanged });
173
+ return {
174
+ location,
175
+ employer,
176
+ client,
177
+ working_hours: supplied?.working_hours,
178
+ timezone,
179
+ };
116
180
  }
117
181
  export function validateAvailabilityWindows(inputs, context, minimum = clientAvailabilityMinimumWindows) {
118
182
  if (inputs.length < minimum)
@@ -122,10 +186,11 @@ export function validateAvailabilityWindows(inputs, context, minimum = clientAva
122
186
  if (input.employer_id !== context.employer.id ||
123
187
  input.location_id !== context.location.id ||
124
188
  input.timezone !== context.timezone ||
125
- minutesOfDay(input.from) <
126
- context.company.work_day_start_hour * minutesPerHour ||
127
- minutesOfDay(input.to) >
128
- context.company.work_day_end_hour * minutesPerHour ||
189
+ (context.working_hours &&
190
+ (minutesOfDay(input.from) <
191
+ context.working_hours.work_day_start_hour * minutesPerHour ||
192
+ minutesOfDay(input.to) >
193
+ context.working_hours.work_day_end_hour * minutesPerHour)) ||
129
194
  inputs
130
195
  .slice(0, index)
131
196
  .some((other) => sameCalendarDay(input.from, other.from) &&
@@ -136,13 +201,16 @@ export function validateAvailabilityWindows(inputs, context, minimum = clientAva
136
201
  }
137
202
  // A chat response contains references, never authority or an appointment confirmation.
138
203
  // Reload each chosen record before preparing the response, including after a resumed batch.
139
- export async function loadAvailabilitySelection(readers, actor, reference, ids, signal) {
204
+ export async function loadAvailabilitySelection(readers, actor, reference, ids, options = {}) {
140
205
  if (ids.length < clientAvailabilityMinimumWindows ||
141
206
  normalizeIDs(ids).length !== ids.length)
142
207
  throw errValidation({ msg: msgAvailabilityData });
143
- const context = await loadAvailabilityContext(readers, actor, reference, signal);
208
+ const context = await loadAvailabilityContext(readers, actor, reference, options);
144
209
  const windows = await Promise.all(ids.map(async (id) => {
145
- const window = (await readers.routes.findOneClientAvailability({ id, signal })).value;
210
+ const window = (await readers.routes.findOneClientAvailability({
211
+ id,
212
+ signal: options.signal,
213
+ })).value;
146
214
  if (!window ||
147
215
  window.id !== id ||
148
216
  window.owner_id !== reference.client_id)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.148",
3
+ "version": "0.0.150",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {