@scalemule/news-publication 0.0.76 → 0.0.77

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.
@@ -18,7 +18,7 @@ declare function MeetupDetail({ meetup, joined }: {
18
18
  joined: boolean;
19
19
  }): React.JSX.Element;
20
20
  declare function activityChoices(): {
21
- value: "other" | "walk" | "games" | "coffee" | "hike" | "pickleball" | "tennis" | "soccer" | "bike" | "dogs" | "books" | "kids" | "volunteer";
21
+ value: "walk" | "other" | "games" | "coffee" | "hike" | "pickleball" | "tennis" | "soccer" | "bike" | "dogs" | "books" | "kids" | "volunteer";
22
22
  label: string;
23
23
  }[];
24
24
 
@@ -18,7 +18,7 @@ declare function MeetupDetail({ meetup, joined }: {
18
18
  joined: boolean;
19
19
  }): React.JSX.Element;
20
20
  declare function activityChoices(): {
21
- value: "other" | "walk" | "games" | "coffee" | "hike" | "pickleball" | "tennis" | "soccer" | "bike" | "dogs" | "books" | "kids" | "volunteer";
21
+ value: "walk" | "other" | "games" | "coffee" | "hike" | "pickleball" | "tennis" | "soccer" | "bike" | "dogs" | "books" | "kids" | "volunteer";
22
22
  label: string;
23
23
  }[];
24
24
 
@@ -150,19 +150,64 @@ function greetingText(profile, now = /* @__PURE__ */ new Date(), fallback = "Wel
150
150
  if (!name) return fallback;
151
151
  return `${greetingWord(localHour(now, profile.time_zone, profile.locale))}, ${name}`;
152
152
  }
153
+ var READER_GREETING_POLL_MS = 3e4;
154
+ var READER_GREETING_TIMEOUT_MS = 8e3;
155
+ var READER_IDENTITY_CHANGED_EVENT = "scalemule:reader-identity-changed";
153
156
  function ReaderGreeting({ endpoint = "/api/news/session", className, fallback = "Welcome back", as: Tag = "span" }) {
154
157
  const [text, setText] = react.useState(null);
155
158
  react.useEffect(() => {
156
159
  let alive = true;
157
- const controller = new AbortController();
158
- fetch(endpoint, { credentials: "same-origin", cache: "no-store", signal: controller.signal }).then((response) => response.ok ? response.json() : null).then((summary) => {
159
- if (!alive || !summary || summary.authenticated !== true) return;
160
- setText(greetingText(summary, /* @__PURE__ */ new Date(), fallback));
161
- }).catch(() => {
162
- });
160
+ let generation = 0;
161
+ let authenticated = false;
162
+ let controller;
163
+ let deadline;
164
+ const refresh = (clear = false) => {
165
+ if (!alive) return;
166
+ if (clear) {
167
+ authenticated = false;
168
+ setText(null);
169
+ }
170
+ const current = ++generation;
171
+ controller?.abort();
172
+ if (deadline !== void 0) window.clearTimeout(deadline);
173
+ if (document.visibilityState === "hidden") return;
174
+ const request = new AbortController();
175
+ controller = request;
176
+ const timeout = window.setTimeout(() => {
177
+ if (!alive || current !== generation) return;
178
+ generation++;
179
+ authenticated = false;
180
+ setText(null);
181
+ request.abort();
182
+ }, READER_GREETING_TIMEOUT_MS);
183
+ deadline = timeout;
184
+ fetch(endpoint, { credentials: "same-origin", cache: "no-store", signal: request.signal }).then((response) => response.ok ? response.json() : null).then((summary) => {
185
+ if (!alive || current !== generation) return;
186
+ authenticated = summary?.authenticated === true;
187
+ setText(authenticated && summary ? greetingText(summary, /* @__PURE__ */ new Date(), fallback) : null);
188
+ }).catch(() => {
189
+ if (!alive || current !== generation) return;
190
+ authenticated = false;
191
+ setText(null);
192
+ }).finally(() => window.clearTimeout(timeout));
193
+ };
194
+ const recheck = () => refresh(true);
195
+ const timer = window.setInterval(() => {
196
+ if (authenticated) refresh();
197
+ }, READER_GREETING_POLL_MS);
198
+ window.addEventListener("focus", recheck);
199
+ document.addEventListener("visibilitychange", recheck);
200
+ document.addEventListener(READER_IDENTITY_CHANGED_EVENT, recheck);
201
+ refresh(true);
163
202
  return () => {
164
203
  alive = false;
165
- controller.abort();
204
+ generation++;
205
+ controller?.abort();
206
+ if (deadline !== void 0) window.clearTimeout(deadline);
207
+ window.clearInterval(timer);
208
+ window.removeEventListener("focus", recheck);
209
+ document.removeEventListener("visibilitychange", recheck);
210
+ document.removeEventListener(READER_IDENTITY_CHANGED_EVENT, recheck);
166
211
  };
167
212
  }, [endpoint, fallback]);
168
213
  if (text === null) return null;
@@ -350,6 +395,9 @@ exports.AccountLinkForm = AccountLinkForm;
350
395
  exports.AccountSwitchPrompt = AccountSwitchPrompt;
351
396
  exports.LEDVERY_BUTTON_LABELS = LEDVERY_BUTTON_LABELS;
352
397
  exports.LedveryButton = LedveryButton;
398
+ exports.READER_GREETING_POLL_MS = READER_GREETING_POLL_MS;
399
+ exports.READER_GREETING_TIMEOUT_MS = READER_GREETING_TIMEOUT_MS;
400
+ exports.READER_IDENTITY_CHANGED_EVENT = READER_IDENTITY_CHANGED_EVENT;
353
401
  exports.READER_SSO_ATTEMPT_WINDOW_MS = READER_SSO_ATTEMPT_WINDOW_MS;
354
402
  exports.READER_SSO_BOT_PATTERN = READER_SSO_BOT_PATTERN;
355
403
  exports.READER_SSO_PRIVATE_PATHS = READER_SSO_PRIVATE_PATHS;
@@ -64,6 +64,9 @@ type SessionSummaryResponse = {
64
64
  locale?: string;
65
65
  time_zone?: string;
66
66
  };
67
+ declare const READER_GREETING_POLL_MS = 30000;
68
+ declare const READER_GREETING_TIMEOUT_MS = 8000;
69
+ declare const READER_IDENTITY_CHANGED_EVENT = "scalemule:reader-identity-changed";
67
70
  declare function ReaderGreeting({ endpoint, className, fallback, as: Tag }: {
68
71
  endpoint?: string;
69
72
  className?: string;
@@ -128,4 +131,4 @@ declare function AccountSwitchPrompt({ currentName, pendingName, endpoint, class
128
131
  onDone?: (result: SwitchResult) => void;
129
132
  }): React.JSX.Element;
130
133
 
131
- export { AccountLinkForm, AccountSwitchPrompt, type GreetingProfile, LEDVERY_BUTTON_LABELS, LedveryButton, type LinkResult, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, type ReaderSsoClassNames, type ReaderSsoProvider, type SessionSummaryResponse, type SilentSignInDecision, type SilentSignInEnvironment, type SwitchChoice, type SwitchResult, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
134
+ export { AccountLinkForm, AccountSwitchPrompt, type GreetingProfile, LEDVERY_BUTTON_LABELS, LedveryButton, type LinkResult, READER_GREETING_POLL_MS, READER_GREETING_TIMEOUT_MS, READER_IDENTITY_CHANGED_EVENT, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, type ReaderSsoClassNames, type ReaderSsoProvider, type SessionSummaryResponse, type SilentSignInDecision, type SilentSignInEnvironment, type SwitchChoice, type SwitchResult, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
@@ -64,6 +64,9 @@ type SessionSummaryResponse = {
64
64
  locale?: string;
65
65
  time_zone?: string;
66
66
  };
67
+ declare const READER_GREETING_POLL_MS = 30000;
68
+ declare const READER_GREETING_TIMEOUT_MS = 8000;
69
+ declare const READER_IDENTITY_CHANGED_EVENT = "scalemule:reader-identity-changed";
67
70
  declare function ReaderGreeting({ endpoint, className, fallback, as: Tag }: {
68
71
  endpoint?: string;
69
72
  className?: string;
@@ -128,4 +131,4 @@ declare function AccountSwitchPrompt({ currentName, pendingName, endpoint, class
128
131
  onDone?: (result: SwitchResult) => void;
129
132
  }): React.JSX.Element;
130
133
 
131
- export { AccountLinkForm, AccountSwitchPrompt, type GreetingProfile, LEDVERY_BUTTON_LABELS, LedveryButton, type LinkResult, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, type ReaderSsoClassNames, type ReaderSsoProvider, type SessionSummaryResponse, type SilentSignInDecision, type SilentSignInEnvironment, type SwitchChoice, type SwitchResult, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
134
+ export { AccountLinkForm, AccountSwitchPrompt, type GreetingProfile, LEDVERY_BUTTON_LABELS, LedveryButton, type LinkResult, READER_GREETING_POLL_MS, READER_GREETING_TIMEOUT_MS, READER_IDENTITY_CHANGED_EVENT, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, type ReaderSsoClassNames, type ReaderSsoProvider, type SessionSummaryResponse, type SilentSignInDecision, type SilentSignInEnvironment, type SwitchChoice, type SwitchResult, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
@@ -148,19 +148,64 @@ function greetingText(profile, now = /* @__PURE__ */ new Date(), fallback = "Wel
148
148
  if (!name) return fallback;
149
149
  return `${greetingWord(localHour(now, profile.time_zone, profile.locale))}, ${name}`;
150
150
  }
151
+ var READER_GREETING_POLL_MS = 3e4;
152
+ var READER_GREETING_TIMEOUT_MS = 8e3;
153
+ var READER_IDENTITY_CHANGED_EVENT = "scalemule:reader-identity-changed";
151
154
  function ReaderGreeting({ endpoint = "/api/news/session", className, fallback = "Welcome back", as: Tag = "span" }) {
152
155
  const [text, setText] = useState(null);
153
156
  useEffect(() => {
154
157
  let alive = true;
155
- const controller = new AbortController();
156
- fetch(endpoint, { credentials: "same-origin", cache: "no-store", signal: controller.signal }).then((response) => response.ok ? response.json() : null).then((summary) => {
157
- if (!alive || !summary || summary.authenticated !== true) return;
158
- setText(greetingText(summary, /* @__PURE__ */ new Date(), fallback));
159
- }).catch(() => {
160
- });
158
+ let generation = 0;
159
+ let authenticated = false;
160
+ let controller;
161
+ let deadline;
162
+ const refresh = (clear = false) => {
163
+ if (!alive) return;
164
+ if (clear) {
165
+ authenticated = false;
166
+ setText(null);
167
+ }
168
+ const current = ++generation;
169
+ controller?.abort();
170
+ if (deadline !== void 0) window.clearTimeout(deadline);
171
+ if (document.visibilityState === "hidden") return;
172
+ const request = new AbortController();
173
+ controller = request;
174
+ const timeout = window.setTimeout(() => {
175
+ if (!alive || current !== generation) return;
176
+ generation++;
177
+ authenticated = false;
178
+ setText(null);
179
+ request.abort();
180
+ }, READER_GREETING_TIMEOUT_MS);
181
+ deadline = timeout;
182
+ fetch(endpoint, { credentials: "same-origin", cache: "no-store", signal: request.signal }).then((response) => response.ok ? response.json() : null).then((summary) => {
183
+ if (!alive || current !== generation) return;
184
+ authenticated = summary?.authenticated === true;
185
+ setText(authenticated && summary ? greetingText(summary, /* @__PURE__ */ new Date(), fallback) : null);
186
+ }).catch(() => {
187
+ if (!alive || current !== generation) return;
188
+ authenticated = false;
189
+ setText(null);
190
+ }).finally(() => window.clearTimeout(timeout));
191
+ };
192
+ const recheck = () => refresh(true);
193
+ const timer = window.setInterval(() => {
194
+ if (authenticated) refresh();
195
+ }, READER_GREETING_POLL_MS);
196
+ window.addEventListener("focus", recheck);
197
+ document.addEventListener("visibilitychange", recheck);
198
+ document.addEventListener(READER_IDENTITY_CHANGED_EVENT, recheck);
199
+ refresh(true);
161
200
  return () => {
162
201
  alive = false;
163
- controller.abort();
202
+ generation++;
203
+ controller?.abort();
204
+ if (deadline !== void 0) window.clearTimeout(deadline);
205
+ window.clearInterval(timer);
206
+ window.removeEventListener("focus", recheck);
207
+ document.removeEventListener("visibilitychange", recheck);
208
+ document.removeEventListener(READER_IDENTITY_CHANGED_EVENT, recheck);
164
209
  };
165
210
  }, [endpoint, fallback]);
166
211
  if (text === null) return null;
@@ -344,4 +389,4 @@ function AccountSwitchPrompt({
344
389
  ] });
345
390
  }
346
391
 
347
- export { AccountLinkForm, AccountSwitchPrompt, LEDVERY_BUTTON_LABELS, LedveryButton, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
392
+ export { AccountLinkForm, AccountSwitchPrompt, LEDVERY_BUTTON_LABELS, LedveryButton, READER_GREETING_POLL_MS, READER_GREETING_TIMEOUT_MS, READER_IDENTITY_CHANGED_EVENT, READER_SSO_ATTEMPT_WINDOW_MS, READER_SSO_BOT_PATTERN, READER_SSO_PRIVATE_PATHS, ReaderGreeting, ReaderSsoBoot, firstName, greetingText, greetingWord, isFormField, localHour, silentSignInDecision, silentSignInUrl };
@@ -195,7 +195,7 @@ async function startSignIn(config, input) {
195
195
  }
196
196
  const staleSession = [];
197
197
  if (input.cookies(c.names.session)) {
198
- const current = await currentUser(c, input.cookies(c.names.session));
198
+ const current = await currentUser(c, input.cookies(c.names.session), mode === "silent" ? "passive" : "active");
199
199
  if (current && current.email_verified) {
200
200
  if (mode === "silent" && (!current.needsSsoUpgrade || keepsLocal(input.cookies(c.names.ssoChecked), input.cookies(c.names.session)))) return { ok: false, status: 204, error: "", reason: "session", redirect, cookies: [] };
201
201
  } else {
@@ -237,9 +237,9 @@ async function startSignIn(config, input) {
237
237
  return { ok: false, status: 503, error: "Ledvery sign-in is temporarily unavailable. Please try again shortly or use your existing reader account.", reason: "unavailable", redirect, cookies: [] };
238
238
  }
239
239
  }
240
- async function currentUser(c, token) {
240
+ async function currentUser(c, token, activity = "active") {
241
241
  if (!token || !SESSION_TOKEN.test(token)) return null;
242
- const response = await c.request("/v1/auth/me", { method: "GET", token });
242
+ const response = await c.request(activity === "passive" ? "/v1/auth/me?passive=true" : "/v1/auth/me", { method: "GET", token });
243
243
  if (failed(response)) return null;
244
244
  const me = payload(response);
245
245
  if (typeof me.id !== "string" || !me.id) return null;
@@ -468,7 +468,7 @@ async function signOut(config, input) {
468
468
  }
469
469
  async function sessionSummary(config, cookies) {
470
470
  const c = resolve(config);
471
- const user = await currentUser(c, cookies(c.names.session));
471
+ const user = await currentUser(c, cookies(c.names.session), "passive");
472
472
  if (!user || !user.email_verified) return { authenticated: false };
473
473
  return { authenticated: true, full_name: user.full_name, locale: user.locale, time_zone: user.time_zone, ...user.needsSsoUpgrade && !keepsLocal(cookies(c.names.ssoChecked), cookies(c.names.session)) ? { session_upgrade_required: true } : {} };
474
474
  }
@@ -180,7 +180,7 @@ async function startSignIn(config, input) {
180
180
  }
181
181
  const staleSession = [];
182
182
  if (input.cookies(c.names.session)) {
183
- const current = await currentUser(c, input.cookies(c.names.session));
183
+ const current = await currentUser(c, input.cookies(c.names.session), mode === "silent" ? "passive" : "active");
184
184
  if (current && current.email_verified) {
185
185
  if (mode === "silent" && (!current.needsSsoUpgrade || keepsLocal(input.cookies(c.names.ssoChecked), input.cookies(c.names.session)))) return { ok: false, status: 204, error: "", reason: "session", redirect, cookies: [] };
186
186
  } else {
@@ -222,9 +222,9 @@ async function startSignIn(config, input) {
222
222
  return { ok: false, status: 503, error: "Ledvery sign-in is temporarily unavailable. Please try again shortly or use your existing reader account.", reason: "unavailable", redirect, cookies: [] };
223
223
  }
224
224
  }
225
- async function currentUser(c, token) {
225
+ async function currentUser(c, token, activity = "active") {
226
226
  if (!token || !SESSION_TOKEN.test(token)) return null;
227
- const response = await c.request("/v1/auth/me", { method: "GET", token });
227
+ const response = await c.request(activity === "passive" ? "/v1/auth/me?passive=true" : "/v1/auth/me", { method: "GET", token });
228
228
  if (failed(response)) return null;
229
229
  const me = payload(response);
230
230
  if (typeof me.id !== "string" || !me.id) return null;
@@ -453,7 +453,7 @@ async function signOut(config, input) {
453
453
  }
454
454
  async function sessionSummary(config, cookies) {
455
455
  const c = resolve(config);
456
- const user = await currentUser(c, cookies(c.names.session));
456
+ const user = await currentUser(c, cookies(c.names.session), "passive");
457
457
  if (!user || !user.email_verified) return { authenticated: false };
458
458
  return { authenticated: true, full_name: user.full_name, locale: user.locale, time_zone: user.time_zone, ...user.needsSsoUpgrade && !keepsLocal(cookies(c.names.ssoChecked), cookies(c.names.session)) ? { session_upgrade_required: true } : {} };
459
459
  }
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.76",
3
+ "version": "0.0.77",
4
4
  "description": "Shared server framework for ScaleMule local news publications. Walnut Creek Times is the broadsheet default; each paper supplies its own identity and feature flags.",
5
5
  "type": "module",
6
- "bin": { "scalemule-article": "./bin/scalemule-article.mjs" },
6
+ "bin": {
7
+ "scalemule-article": "./bin/scalemule-article.mjs"
8
+ },
7
9
  "main": "./dist/index.cjs",
8
10
  "module": "./dist/index.js",
9
11
  "types": "./dist/index.d.ts",