@symbiote-native/notifications 0.1.1 → 0.1.3

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.
Files changed (100) hide show
  1. package/README.md +94 -58
  2. package/build/angular/index.d.ts +1 -0
  3. package/build/angular/index.js +1 -0
  4. package/build/angular/services/last-notification-response.service/index.d.ts +8 -0
  5. package/build/angular/services/last-notification-response.service/index.js +78 -0
  6. package/build/core/auto-registration.d.ts +5 -0
  7. package/build/core/auto-registration.js +65 -0
  8. package/build/core/backoff.d.ts +6 -0
  9. package/build/core/backoff.js +15 -0
  10. package/build/core/device-token.d.ts +7 -0
  11. package/build/core/device-token.js +35 -0
  12. package/build/core/emitter.d.ts +4 -0
  13. package/build/core/emitter.js +8 -0
  14. package/build/core/index.d.ts +3 -1
  15. package/build/core/index.js +3 -1
  16. package/build/core/last-notification-response.d.ts +2 -0
  17. package/build/core/last-notification-response.js +12 -0
  18. package/build/core/registration-abort.d.ts +2 -0
  19. package/build/core/registration-abort.js +10 -0
  20. package/build/core/token-metadata.d.ts +4 -0
  21. package/build/core/token-metadata.js +34 -0
  22. package/build/core/tokens.d.ts +4 -22
  23. package/build/core/tokens.js +41 -99
  24. package/build/core/update-device-push-token.d.ts +3 -0
  25. package/build/core/update-device-push-token.js +122 -0
  26. package/build/react/hooks/use-last-notification-response/index.d.ts +2 -0
  27. package/build/react/hooks/use-last-notification-response/index.js +22 -0
  28. package/build/react/index.d.ts +2 -0
  29. package/build/react/index.js +2 -0
  30. package/build/solid/index.d.ts +2 -0
  31. package/build/solid/index.js +2 -0
  32. package/build/solid/primitives/create-last-notification-response.d.ts +3 -0
  33. package/build/solid/primitives/create-last-notification-response.js +20 -0
  34. package/build/svelte/index.d.ts +2 -0
  35. package/build/svelte/index.js +2 -0
  36. package/build/svelte/use-last-notification-response.svelte.d.ts +4 -0
  37. package/build/svelte/use-last-notification-response.svelte.js +25 -0
  38. package/build/vue/composables/use-last-notification-response/index.d.ts +3 -0
  39. package/build/vue/composables/use-last-notification-response/index.js +20 -0
  40. package/build/vue/index.d.ts +2 -0
  41. package/build/vue/index.js +2 -0
  42. package/build-ngc/angular/index.d.ts +1 -0
  43. package/build-ngc/angular/index.js +1 -0
  44. package/build-ngc/angular/index.js.map +1 -1
  45. package/build-ngc/angular/services/last-notification-response.service/index.d.ts +11 -0
  46. package/build-ngc/angular/services/last-notification-response.service/index.js +37 -0
  47. package/build-ngc/angular/services/last-notification-response.service/index.js.map +1 -0
  48. package/build-ngc/core/auto-registration.d.ts +5 -0
  49. package/build-ngc/core/auto-registration.js +66 -0
  50. package/build-ngc/core/auto-registration.js.map +1 -0
  51. package/build-ngc/core/backoff.d.ts +6 -0
  52. package/build-ngc/core/backoff.js +16 -0
  53. package/build-ngc/core/backoff.js.map +1 -0
  54. package/build-ngc/core/device-token.d.ts +7 -0
  55. package/build-ngc/core/device-token.js +36 -0
  56. package/build-ngc/core/device-token.js.map +1 -0
  57. package/build-ngc/core/emitter.d.ts +4 -0
  58. package/build-ngc/core/emitter.js +8 -0
  59. package/build-ngc/core/emitter.js.map +1 -1
  60. package/build-ngc/core/index.d.ts +3 -1
  61. package/build-ngc/core/index.js +3 -1
  62. package/build-ngc/core/index.js.map +1 -1
  63. package/build-ngc/core/last-notification-response.d.ts +2 -0
  64. package/build-ngc/core/last-notification-response.js +13 -0
  65. package/build-ngc/core/last-notification-response.js.map +1 -0
  66. package/build-ngc/core/registration-abort.d.ts +2 -0
  67. package/build-ngc/core/registration-abort.js +11 -0
  68. package/build-ngc/core/registration-abort.js.map +1 -0
  69. package/build-ngc/core/token-metadata.d.ts +4 -0
  70. package/build-ngc/core/token-metadata.js +35 -0
  71. package/build-ngc/core/token-metadata.js.map +1 -0
  72. package/build-ngc/core/tokens.d.ts +4 -22
  73. package/build-ngc/core/tokens.js +41 -99
  74. package/build-ngc/core/tokens.js.map +1 -1
  75. package/build-ngc/core/update-device-push-token.d.ts +3 -0
  76. package/build-ngc/core/update-device-push-token.js +123 -0
  77. package/build-ngc/core/update-device-push-token.js.map +1 -0
  78. package/package.json +24 -23
  79. package/src/angular/index.ts +1 -0
  80. package/src/angular/services/last-notification-response.service/index.ts +54 -0
  81. package/src/core/auto-registration.ts +105 -0
  82. package/src/core/backoff.ts +24 -0
  83. package/src/core/device-token.ts +54 -0
  84. package/src/core/emitter.ts +10 -0
  85. package/src/core/index.ts +8 -0
  86. package/src/core/last-notification-response.ts +19 -0
  87. package/src/core/registration-abort.ts +12 -0
  88. package/src/core/token-metadata.ts +42 -0
  89. package/src/core/tokens.ts +71 -133
  90. package/src/core/update-device-push-token.ts +173 -0
  91. package/src/react/hooks/use-last-notification-response/index.ts +35 -0
  92. package/src/react/index.ts +2 -0
  93. package/src/solid/index.ts +2 -0
  94. package/src/solid/primitives/create-last-notification-response.ts +35 -0
  95. package/src/svelte/index.ts +2 -0
  96. package/src/svelte/svelte-ambient.d.ts +3 -0
  97. package/src/svelte/svelte-compile.test-helper.ts +7 -0
  98. package/src/svelte/use-last-notification-response.svelte.ts +45 -0
  99. package/src/vue/composables/use-last-notification-response/index.ts +35 -0
  100. package/src/vue/index.ts +2 -0
@@ -1,78 +1,35 @@
1
- // Ported from expo-notifications @ sdk-57's getDevicePushTokenAsync.ts, getExpoPushTokenAsync.ts,
2
- // unregisterForNotificationsAsync.ts, topicSubscription.ts, TokenEmitter.ts, and the
3
- // `setAutoServerRegistrationEnabledAsync` half of DevicePushTokenAutoRegistration.fx.ts.
4
- //
5
- // ponytail: upstream's DevicePushTokenAutoRegistration.fx.ts also wires a permanent, module-load
6
- // background daemon that re-POSTs the device token to Expo's push backend on every token change
7
- // (with exponential backoff, via `abort-controller`) whenever registration was left enabled.
8
- // That auto-resync daemon is NOT ported — an app that wants it can call
9
- // `getExpoPushTokenAsync()` again from its own `addPushTokenListener` callback, which is the
10
- // same effect without a permanent background retry loop and its extra dependency.
11
- import {
12
- CodedError,
13
- Platform,
14
- UnavailabilityError,
15
- type EventSubscription,
16
- } from 'expo-modules-core';
1
+ // Порт токенов из upstream sdk-57, фоновый ресинк токена живёт в `auto-registration.ts`
2
+ import { applicationId } from '@symbiote-native/application';
3
+ import { CodedError, UnavailabilityError } from 'expo-modules-core';
17
4
 
18
5
  import { dlog } from '@symbiote-native/engine';
19
6
 
7
+ import { installPushTokenAutoRegistration } from './auto-registration';
8
+ import { getDevicePushTokenAsync } from './device-token';
20
9
  import {
21
10
  pushTokenManager,
22
11
  serverRegistrationModule,
23
12
  topicSubscriptionModule,
24
13
  } from './native-modules';
14
+ import { abortPendingRegistration } from './registration-abort';
15
+ import {
16
+ getDeviceIdAsync,
17
+ getTypeOfToken,
18
+ shouldUseDevelopmentNotificationService,
19
+ } from './token-metadata';
25
20
  import type {
26
21
  IDevicePushToken,
27
22
  IExpoPushToken,
28
23
  IExpoPushTokenOptions,
29
24
  } from './types';
30
25
 
31
- const PRODUCTION_BASE_URL = 'https://exp.host/--/api/v2/';
32
-
33
- // This package targets iOS/Android only (root CLAUDE.md); narrow `Platform.OS` at the one
34
- // boundary that needs it rather than casting at every call site.
35
- function nativePlatform(): 'ios' | 'android' {
36
- if (Platform.OS === 'ios' || Platform.OS === 'android') {
37
- return Platform.OS;
38
- }
39
- throw new Error(`[notifications] unsupported platform: ${Platform.OS}`);
40
- }
41
-
42
- export type IPushTokenListener = (token: IDevicePushToken) => void;
43
-
44
- /** Fires whenever the OS rolls the device push token while the app is running. */
45
- export function addPushTokenListener(
46
- listener: IPushTokenListener,
47
- ): EventSubscription {
48
- return pushTokenManager.addListener(
49
- 'onDevicePushToken',
50
- ({ devicePushToken }) => {
51
- listener({ data: devicePushToken, type: nativePlatform() });
52
- },
53
- );
54
- }
55
-
56
- let nativeTokenPromise: Promise<string> | null = null;
26
+ export {
27
+ addPushTokenListener,
28
+ getDevicePushTokenAsync,
29
+ type IPushTokenListener,
30
+ } from './device-token';
57
31
 
58
- /** Returns the native FCM (Android) or APNs (iOS) device push token. */
59
- export async function getDevicePushTokenAsync(): Promise<IDevicePushToken> {
60
- if (!pushTokenManager.getDevicePushTokenAsync) {
61
- throw new UnavailabilityError('Notifications', 'getDevicePushTokenAsync');
62
- }
63
-
64
- let devicePushToken: string;
65
- if (nativeTokenPromise) {
66
- devicePushToken = await nativeTokenPromise;
67
- } else {
68
- nativeTokenPromise = pushTokenManager.getDevicePushTokenAsync();
69
- devicePushToken = await nativeTokenPromise;
70
- nativeTokenPromise = null;
71
- }
72
-
73
- dlog('[notifications] getDevicePushTokenAsync resolved');
74
- return { type: nativePlatform(), data: devicePushToken };
75
- }
32
+ const PRODUCTION_BASE_URL = 'https://exp.host/--/api/v2/';
76
33
 
77
34
  /** Unregisters the device from receiving remote push notifications entirely. */
78
35
  export async function unregisterForNotificationsAsync(): Promise<void> {
@@ -101,14 +58,13 @@ export async function unsubscribeFromTopicAsync(topic: string): Promise<null> {
101
58
  return topicSubscriptionModule.unsubscribeFromTopicAsync(topic);
102
59
  }
103
60
 
104
- /**
105
- * Enables/disables letting the OS-level registration blob persist whether the device push
106
- * token should keep being pushed to the Expo push service backend. See the module doc comment
107
- * above for what this deliberately does *not* do (the auto-resync daemon).
108
- */
61
+ /** Persists whether the device push token keeps being pushed to the Expo push service */
109
62
  export async function setAutoServerRegistrationEnabledAsync(
110
63
  enabled: boolean,
111
64
  ): Promise<void> {
65
+ // Перезаписываем регистрацию, поэтому висящий запрос не должен завершиться
66
+ abortPendingRegistration();
67
+
112
68
  if (!serverRegistrationModule.setRegistrationInfoAsync) {
113
69
  throw new UnavailabilityError('Notifications', 'setRegistrationInfoAsync');
114
70
  }
@@ -131,29 +87,7 @@ export async function setAutoServerRegistrationEnabledAsync(
131
87
  await serverRegistrationModule.setRegistrationInfoAsync(
132
88
  JSON.stringify(existing),
133
89
  );
134
- }
135
-
136
- async function getDeviceIdAsync(): Promise<string> {
137
- try {
138
- if (!serverRegistrationModule.getInstallationIdAsync) {
139
- throw new UnavailabilityError('Notifications', 'getInstallationIdAsync');
140
- }
141
- return await serverRegistrationModule.getInstallationIdAsync();
142
- } catch (error) {
143
- throw new CodedError(
144
- 'ERR_NOTIF_DEVICE_ID',
145
- `Could not fetch the installation ID of the application: ${error}.`,
146
- );
147
- }
148
- }
149
-
150
- function getTypeOfToken(devicePushToken: IDevicePushToken): string {
151
- switch (devicePushToken.type) {
152
- case 'ios':
153
- return 'apns';
154
- case 'android':
155
- return 'fcm';
156
- }
90
+ installPushTokenAutoRegistration();
157
91
  }
158
92
 
159
93
  function getDeviceToken(devicePushToken: IDevicePushToken): string {
@@ -189,50 +123,10 @@ function extractExpoPushToken(data: unknown): string {
189
123
  );
190
124
  }
191
125
 
192
- /**
193
- * Returns an Expo push token usable with Expo's push notification relay service. Makes a
194
- * network request to Expo's servers — wrap in try/catch and retry once the device is back
195
- * online.
196
- *
197
- * ponytail: upstream auto-fills `projectId`/`applicationId`/`development` from
198
- * `expo-constants`/`expo-application`. Neither package is part of this port, so pass them
199
- * explicitly (`applicationId` from `@symbiote-native/application`'s own `applicationId` export
200
- * covers the second one).
201
- */
202
- export async function getExpoPushTokenAsync(
203
- options: IExpoPushTokenOptions = {},
204
- ): Promise<IExpoPushToken> {
205
- const devicePushToken =
206
- options.devicePushToken ?? (await getDevicePushTokenAsync());
207
- const deviceId = options.deviceId ?? (await getDeviceIdAsync());
208
-
209
- if (!options.projectId) {
210
- throw new CodedError(
211
- 'ERR_NOTIFICATIONS_NO_EXPERIENCE_ID',
212
- 'No "projectId" was passed to getExpoPushTokenAsync(). This port does not read expo-constants — pass it explicitly.',
213
- );
214
- }
215
- if (!options.applicationId) {
216
- throw new CodedError(
217
- 'ERR_NOTIFICATIONS_NO_APPLICATION_ID',
218
- 'No "applicationId" was passed to getExpoPushTokenAsync(). This port does not read expo-application — pass it explicitly (e.g. @symbiote-native/application\'s `applicationId`).',
219
- );
220
- }
221
-
222
- const type = options.type ?? getTypeOfToken(devicePushToken);
223
- const development = options.development ?? false;
224
- const baseUrl = options.baseUrl ?? PRODUCTION_BASE_URL;
225
- const url = options.url ?? `${baseUrl}push/getExpoPushToken`;
226
-
227
- const body = {
228
- type,
229
- deviceId: deviceId.toLowerCase(),
230
- development,
231
- appId: options.applicationId,
232
- deviceToken: getDeviceToken(devicePushToken),
233
- projectId: options.projectId,
234
- };
235
-
126
+ async function requestExpoPushTokenAsync(
127
+ url: string,
128
+ body: Record<string, unknown>,
129
+ ): Promise<string> {
236
130
  let response: Response;
237
131
  try {
238
132
  response = await fetch(url, {
@@ -254,7 +148,51 @@ export async function getExpoPushTokenAsync(
254
148
  );
255
149
  }
256
150
 
257
- const expoPushToken = extractExpoPushToken(await parseJsonResponse(response));
151
+ return extractExpoPushToken(await parseJsonResponse(response));
152
+ }
153
+
154
+ // Вынесено, чтобы `getExpoPushTokenAsync` читался как запрос токена и регистрация
155
+ async function buildExpoPushTokenBody(
156
+ options: IExpoPushTokenOptions,
157
+ devicePushToken: IDevicePushToken,
158
+ ): Promise<Record<string, unknown>> {
159
+ if (!options.projectId) {
160
+ throw new CodedError(
161
+ 'ERR_NOTIFICATIONS_NO_EXPERIENCE_ID',
162
+ 'No "projectId" was passed to getExpoPushTokenAsync(). Pass it explicitly (this port has no expo-constants).',
163
+ );
164
+ }
165
+ const appId = options.applicationId ?? applicationId;
166
+ if (!appId) {
167
+ throw new CodedError(
168
+ 'ERR_NOTIFICATIONS_NO_APPLICATION_ID',
169
+ 'No "applicationId" was passed to getExpoPushTokenAsync() and the app has none of its own.',
170
+ );
171
+ }
172
+
173
+ const deviceId = options.deviceId ?? (await getDeviceIdAsync());
174
+ return {
175
+ type: options.type ?? getTypeOfToken(devicePushToken),
176
+ deviceId: deviceId.toLowerCase(),
177
+ development:
178
+ options.development ?? (await shouldUseDevelopmentNotificationService()),
179
+ appId,
180
+ deviceToken: getDeviceToken(devicePushToken),
181
+ projectId: options.projectId,
182
+ };
183
+ }
184
+
185
+ /** Returns an Expo push token for Expo's push relay, needs network so retry when back online */
186
+ export async function getExpoPushTokenAsync(
187
+ options: IExpoPushTokenOptions = {},
188
+ ): Promise<IExpoPushToken> {
189
+ const devicePushToken =
190
+ options.devicePushToken ?? (await getDevicePushTokenAsync());
191
+ const body = await buildExpoPushTokenBody(options, devicePushToken);
192
+ const baseUrl = options.baseUrl ?? PRODUCTION_BASE_URL;
193
+ const url = options.url ?? `${baseUrl}push/getExpoPushToken`;
194
+
195
+ const expoPushToken = await requestExpoPushTokenAsync(url, body);
258
196
 
259
197
  if (!options.url && !options.baseUrl) {
260
198
  try {
@@ -0,0 +1,173 @@
1
+ // Порт `utils/updateDevicePushTokenAsync.ts` из upstream sdk-57
2
+ import { applicationId } from '@symbiote-native/application';
3
+ import { dlog } from '@symbiote-native/engine';
4
+
5
+ import { computeNextBackoffInterval } from './backoff';
6
+ import { serverRegistrationModule } from './native-modules';
7
+ import {
8
+ getDeviceIdAsync,
9
+ getTypeOfToken,
10
+ shouldUseDevelopmentNotificationService,
11
+ } from './token-metadata';
12
+ import type { IDevicePushToken } from './types';
13
+
14
+ const UPDATE_DEVICE_PUSH_TOKEN_URL =
15
+ 'https://exp.host/--/api/v2/push/updateDeviceToken';
16
+ const LAST_TOKEN_KEY = 'lastRegisteredDeviceToken';
17
+ const INITIAL_BACKOFF_MS = 500;
18
+ const MAX_BACKOFF_MS = 2 * 60 * 1000;
19
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
20
+
21
+ // Повторная регистрация раз в 7 дней, т.к. сервер мог потерять запись об устройстве
22
+ const REGISTRATION_TTL_MS = 7 * MS_PER_DAY;
23
+
24
+ type IStoredTokenData = {
25
+ deviceToken: string;
26
+ appId: string | null;
27
+ development: boolean;
28
+ type: string;
29
+ registeredAt: number;
30
+ };
31
+
32
+ function isRecord(value: unknown): value is Record<string, unknown> {
33
+ return typeof value === 'object' && value !== null;
34
+ }
35
+
36
+ function isStoredTokenData(value: unknown): value is IStoredTokenData {
37
+ return (
38
+ isRecord(value) &&
39
+ typeof value.deviceToken === 'string' &&
40
+ typeof value.development === 'boolean' &&
41
+ typeof value.type === 'string' &&
42
+ typeof value.registeredAt === 'number'
43
+ );
44
+ }
45
+
46
+ async function getLastRegisteredTokenDataAsync(): Promise<IStoredTokenData | null> {
47
+ try {
48
+ if (!serverRegistrationModule.getRegistrationInfoAsync) return null;
49
+ const info = await serverRegistrationModule.getRegistrationInfoAsync();
50
+ if (!info) return null;
51
+ const stored: unknown = JSON.parse(info)?.[LAST_TOKEN_KEY];
52
+ return isStoredTokenData(stored) ? stored : null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ async function setLastRegisteredTokenDataAsync(
59
+ tokenData: IStoredTokenData,
60
+ ): Promise<void> {
61
+ try {
62
+ if (
63
+ !serverRegistrationModule.getRegistrationInfoAsync ||
64
+ !serverRegistrationModule.setRegistrationInfoAsync
65
+ ) {
66
+ return;
67
+ }
68
+ const info = await serverRegistrationModule.getRegistrationInfoAsync();
69
+ const existing = info ? JSON.parse(info) : {};
70
+ existing[LAST_TOKEN_KEY] = tokenData;
71
+ await serverRegistrationModule.setRegistrationInfoAsync(
72
+ JSON.stringify(existing),
73
+ );
74
+ } catch {
75
+ // Best-effort, при следующем запуске приложение зарегистрируется снова
76
+ }
77
+ }
78
+
79
+ // Если проверка невозможна, считаем токен изменившимся (fail-open)
80
+ export async function hasDeviceTokenChangedAsync(
81
+ token: IDevicePushToken,
82
+ ): Promise<boolean> {
83
+ try {
84
+ const development = await shouldUseDevelopmentNotificationService();
85
+ const lastTokenData = await getLastRegisteredTokenDataAsync();
86
+
87
+ if (lastTokenData == null) return true;
88
+
89
+ const age = Date.now() - lastTokenData.registeredAt;
90
+ if (age < 0 || age >= REGISTRATION_TTL_MS) return true;
91
+
92
+ return (
93
+ token.data !== lastTokenData.deviceToken ||
94
+ applicationId !== lastTokenData.appId ||
95
+ development !== lastTokenData.development ||
96
+ getTypeOfToken(token) !== lastTokenData.type
97
+ );
98
+ } catch {
99
+ return true;
100
+ }
101
+ }
102
+
103
+ // `true` значит запрос надо повторить
104
+ async function sendTokenOnceAsync(
105
+ signal: AbortSignal,
106
+ token: IDevicePushToken,
107
+ ): Promise<boolean> {
108
+ const [development, deviceId] = await Promise.all([
109
+ shouldUseDevelopmentNotificationService(),
110
+ getDeviceIdAsync(),
111
+ ]);
112
+ const type = getTypeOfToken(token);
113
+
114
+ try {
115
+ const response = await fetch(UPDATE_DEVICE_PUSH_TOKEN_URL, {
116
+ method: 'POST',
117
+ headers: { 'content-type': 'application/json' },
118
+ body: JSON.stringify({
119
+ deviceId: deviceId.toLowerCase(),
120
+ development,
121
+ deviceToken: token.data,
122
+ appId: applicationId,
123
+ type,
124
+ }),
125
+ signal,
126
+ });
127
+
128
+ if (!response.ok) {
129
+ dlog(
130
+ `[notifications] device push token update rejected: ${await response.text()}`,
131
+ );
132
+ return true;
133
+ }
134
+
135
+ await setLastRegisteredTokenDataAsync({
136
+ deviceToken: token.data,
137
+ appId: applicationId,
138
+ development,
139
+ type,
140
+ registeredAt: Date.now(),
141
+ });
142
+ return false;
143
+ } catch (error) {
144
+ // Прерванный запрос может упасть с `AbortError` или нативной ошибкой отмены
145
+ if (signal.aborted || (isRecord(error) && error.name === 'AbortError'))
146
+ return false;
147
+
148
+ console.warn(
149
+ '[notifications] Error thrown while updating the device push token with the server:',
150
+ error,
151
+ );
152
+ return true;
153
+ }
154
+ }
155
+
156
+ export async function updateDevicePushTokenAsync(
157
+ signal: AbortSignal,
158
+ token: IDevicePushToken,
159
+ ): Promise<void> {
160
+ const backoffOptions = { maxBackoff: MAX_BACKOFF_MS };
161
+
162
+ for (let retriesCount = 0; !signal.aborted; retriesCount += 1) {
163
+ const shouldRetry = await sendTokenOnceAsync(signal, token);
164
+ if (!shouldRetry || signal.aborted) return;
165
+
166
+ const backoff = computeNextBackoffInterval(
167
+ INITIAL_BACKOFF_MS,
168
+ retriesCount,
169
+ backoffOptions,
170
+ );
171
+ await new Promise<void>(resolve => setTimeout(resolve, backoff));
172
+ }
173
+ }
@@ -0,0 +1,35 @@
1
+ // React twin of expo-notifications' `useLastNotificationResponse`. `useLayoutEffect` matches
2
+ // upstream's own choice - the listener must register before paint
3
+
4
+ import { useLayoutEffect, useState } from 'react';
5
+ import {
6
+ addNotificationResponseClearedListener,
7
+ addNotificationResponseReceivedListener,
8
+ determineNextResponse,
9
+ getLastNotificationResponse,
10
+ } from '../../../core';
11
+ import type { IMaybeNotificationResponse } from '../../../core';
12
+
13
+ export function useLastNotificationResponse(): IMaybeNotificationResponse {
14
+ const [lastResponse, setLastResponse] =
15
+ useState<IMaybeNotificationResponse>(undefined);
16
+
17
+ useLayoutEffect(() => {
18
+ const response = getLastNotificationResponse();
19
+ setLastResponse(prev => determineNextResponse(prev, response));
20
+
21
+ const subscription = addNotificationResponseReceivedListener(next => {
22
+ setLastResponse(prev => determineNextResponse(prev, next));
23
+ });
24
+ const clearedSubscription = addNotificationResponseClearedListener(() => {
25
+ setLastResponse(null);
26
+ });
27
+
28
+ return () => {
29
+ subscription.remove();
30
+ clearedSubscription.remove();
31
+ };
32
+ }, []);
33
+
34
+ return lastResponse;
35
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { useLastNotificationResponse } from './hooks/use-last-notification-response';
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { createLastNotificationResponse } from './primitives/create-last-notification-response';
@@ -0,0 +1,35 @@
1
+ // Solid twin of `../../react`'s `useLastNotificationResponse`
2
+
3
+ import { createSignal, onCleanup, onMount, type Accessor } from 'solid-js';
4
+ import {
5
+ addNotificationResponseClearedListener,
6
+ addNotificationResponseReceivedListener,
7
+ determineNextResponse,
8
+ getLastNotificationResponse,
9
+ } from '../../core';
10
+ import type { IMaybeNotificationResponse } from '../../core';
11
+
12
+ export function createLastNotificationResponse(): Accessor<IMaybeNotificationResponse> {
13
+ const [lastResponse, setLastResponse] =
14
+ createSignal<IMaybeNotificationResponse>(undefined);
15
+
16
+ onMount(() => {
17
+ setLastResponse(prev =>
18
+ determineNextResponse(prev, getLastNotificationResponse()),
19
+ );
20
+
21
+ const subscription = addNotificationResponseReceivedListener(next => {
22
+ setLastResponse(prev => determineNextResponse(prev, next));
23
+ });
24
+ const clearedSubscription = addNotificationResponseClearedListener(() => {
25
+ setLastResponse(null);
26
+ });
27
+
28
+ onCleanup(() => {
29
+ subscription.remove();
30
+ clearedSubscription.remove();
31
+ });
32
+ });
33
+
34
+ return lastResponse;
35
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { useLastNotificationResponse } from './use-last-notification-response.svelte';
@@ -0,0 +1,3 @@
1
+ // Pulls svelte's ambient globals ($state/$effect/...) into this package's tsc program,
2
+ // same fix as `packages/navigation/src/svelte/svelte-ambient.d.ts`
3
+ /// <reference types="svelte" />
@@ -0,0 +1,7 @@
1
+ // Re-exports the existing harness instead of a third copy (packages/sqlite and
2
+ // packages/navigation already carry one, not part of either package's public exports)
3
+ export {
4
+ createSvelteHarness,
5
+ loadComponent,
6
+ type ISvelteHarness,
7
+ } from '../../../sqlite/src/svelte/svelte-compile.test-helper';
@@ -0,0 +1,45 @@
1
+ // Boxed-getter shape matches `@symbiote-native/screen-capture`'s own runes by design - each
2
+ // package owns its copy rather than depend on a sibling install-optional package
3
+
4
+ import { untrack } from 'svelte';
5
+ import {
6
+ addNotificationResponseClearedListener,
7
+ addNotificationResponseReceivedListener,
8
+ determineNextResponse,
9
+ getLastNotificationResponse,
10
+ } from '../core';
11
+ import type { IMaybeNotificationResponse } from '../core';
12
+
13
+ export function useLastNotificationResponse(): {
14
+ readonly current: IMaybeNotificationResponse;
15
+ } {
16
+ let lastResponse = $state<IMaybeNotificationResponse>(undefined);
17
+
18
+ $effect(() => {
19
+ lastResponse = determineNextResponse(
20
+ untrack(() => lastResponse),
21
+ getLastNotificationResponse(),
22
+ );
23
+
24
+ const subscription = addNotificationResponseReceivedListener(next => {
25
+ lastResponse = determineNextResponse(
26
+ untrack(() => lastResponse),
27
+ next,
28
+ );
29
+ });
30
+ const clearedSubscription = addNotificationResponseClearedListener(() => {
31
+ lastResponse = null;
32
+ });
33
+
34
+ return () => {
35
+ subscription.remove();
36
+ clearedSubscription.remove();
37
+ };
38
+ });
39
+
40
+ return {
41
+ get current(): IMaybeNotificationResponse {
42
+ return lastResponse;
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,35 @@
1
+ // Vue twin of `../../../react`'s `useLastNotificationResponse`
2
+
3
+ import { onMounted, onUnmounted, ref, type Ref } from '@vue/runtime-core';
4
+ import {
5
+ addNotificationResponseClearedListener,
6
+ addNotificationResponseReceivedListener,
7
+ determineNextResponse,
8
+ getLastNotificationResponse,
9
+ } from '../../../core';
10
+ import type { IMaybeNotificationResponse } from '../../../core';
11
+
12
+ export function useLastNotificationResponse(): Ref<IMaybeNotificationResponse> {
13
+ const lastResponse = ref<IMaybeNotificationResponse>(undefined);
14
+
15
+ onMounted(() => {
16
+ lastResponse.value = determineNextResponse(
17
+ lastResponse.value,
18
+ getLastNotificationResponse(),
19
+ );
20
+
21
+ const subscription = addNotificationResponseReceivedListener(next => {
22
+ lastResponse.value = determineNextResponse(lastResponse.value, next);
23
+ });
24
+ const clearedSubscription = addNotificationResponseClearedListener(() => {
25
+ lastResponse.value = null;
26
+ });
27
+
28
+ onUnmounted(() => {
29
+ subscription.remove();
30
+ clearedSubscription.remove();
31
+ });
32
+ });
33
+
34
+ return lastResponse;
35
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { useLastNotificationResponse } from './composables/use-last-notification-response';