cursedbelt 5.70.0 → 5.71.0

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.
@@ -39,8 +39,13 @@ export interface NotificationsBinding {
39
39
  * transport feeding rows in, and the store's config hooks pushing
40
40
  * read/dismiss back out.
41
41
  *
42
- * const { store } = createNotificationsBinding();
43
- * <NotificationBell store={store} />
42
+ * const binding = useNotificationsBinding(); // in a component — never during render
43
+ * if (binding) return <NotificationBell store={binding.store} />;
44
+ *
45
+ * 🔴 This function CONNECTS when called, so a component must not call it while
46
+ * rendering: React discards renders, and each discarded one would leak an open
47
+ * stream. {@link useNotificationsBinding} builds it in an effect and disposes it
48
+ * on unmount; call this directly only outside React, or as a page-long singleton.
44
49
  *
45
50
  * ── The one asymmetry worth knowing ─────────────────────────────────────────
46
51
  * Rows flow IN through the transport; acknowledgments flow OUT through these
@@ -1 +1 @@
1
- {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../../src/react/notifications/binding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,8BAA8B,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAGpG,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,KAAK,uBAAuB,EAA8B,MAAM,oBAAoB,CAAC;AAE9F;;gFAEgF;AAChF,MAAM,MAAM,0BAA0B,GAAG,uBAAuB,GAC9D,4BAA4B,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,WAAW,2BAA2B;IAC1C,gFAAgF;IAChF,MAAM,CAAC,EAAE,0BAA0B,CAAC;IACpC,6EAA6E;IAC7E,IAAI,CAAC,EAAE,8BAA8B,CAAC;IACtC,qFAAqF;IACrF,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,eAAe,KAAK,IAAI,CAAC;IACjD,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC1C,+EAA+E;IAC/E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,EAAE,0BAA0B,CAAC;IACnC;oEACgE;IAChE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,2BAAgC,GACxC,oBAAoB,CA4BtB"}
1
+ {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../../src/react/notifications/binding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,8BAA8B,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAGpG,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,KAAK,uBAAuB,EAA8B,MAAM,oBAAoB,CAAC;AAE9F;;gFAEgF;AAChF,MAAM,MAAM,0BAA0B,GAAG,uBAAuB,GAC9D,4BAA4B,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,WAAW,2BAA2B;IAC1C,gFAAgF;IAChF,MAAM,CAAC,EAAE,0BAA0B,CAAC;IACpC,6EAA6E;IAC7E,IAAI,CAAC,EAAE,8BAA8B,CAAC;IACtC,qFAAqF;IACrF,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,eAAe,KAAK,IAAI,CAAC;IACjD,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC1C,+EAA+E;IAC/E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,EAAE,0BAA0B,CAAC;IACnC;oEACgE;IAChE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,2BAAgC,GACxC,oBAAoB,CA4BtB"}
@@ -7,8 +7,13 @@ import { notifyNotificationsChanged } from './pollTransport.js';
7
7
  * transport feeding rows in, and the store's config hooks pushing
8
8
  * read/dismiss back out.
9
9
  *
10
- * const { store } = createNotificationsBinding();
11
- * <NotificationBell store={store} />
10
+ * const binding = useNotificationsBinding(); // in a component — never during render
11
+ * if (binding) return <NotificationBell store={binding.store} />;
12
+ *
13
+ * 🔴 This function CONNECTS when called, so a component must not call it while
14
+ * rendering: React discards renders, and each discarded one would leak an open
15
+ * stream. {@link useNotificationsBinding} builds it in an effect and disposes it
16
+ * on unmount; call this directly only outside React, or as a page-long singleton.
12
17
  *
13
18
  * ── The one asymmetry worth knowing ─────────────────────────────────────────
14
19
  * Rows flow IN through the transport; acknowledgments flow OUT through these
@@ -1 +1 @@
1
- {"version":3,"file":"binding.js","sourceRoot":"","sources":["../../../src/react/notifications/binding.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,kCAAkC,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EACL,uBAAuB,GAIxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAgC,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAoC9F;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,0BAA0B,CACxC,UAAuC,EAAE;IAEzC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,6BAA6B,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,mBAAmB,MAAM,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC;IAElH,MAAM,GAAG,GAAG,CAAC,MAAc,EAAE,IAAsB,EAAQ,EAAE;QAC3D,KAAK,IAAI,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/F,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,uBAAuB,CAAC;QACpC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAClF,aAAa,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACzE,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACjE,UAAU,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;KAC/D,CAAC,CAAC;IAEH,MAAM,SAAS,GACb,OAAO,CAAC,SAAS;QACjB,kCAAkC,CAAC,MAAM,EAAE;YACzC,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;SAC3C,CAAC,CAAC;IAEL,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAE7D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AACpC,CAAC"}
1
+ {"version":3,"file":"binding.js","sourceRoot":"","sources":["../../../src/react/notifications/binding.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,kCAAkC,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EACL,uBAAuB,GAIxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAgC,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAoC9F;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,0BAA0B,CACxC,UAAuC,EAAE;IAEzC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,6BAA6B,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,mBAAmB,MAAM,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC;IAElH,MAAM,GAAG,GAAG,CAAC,MAAc,EAAE,IAAsB,EAAQ,EAAE;QAC3D,KAAK,IAAI,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/F,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,uBAAuB,CAAC;QACpC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAClF,aAAa,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACzE,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACjE,UAAU,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;KAC/D,CAAC,CAAC;IAEH,MAAM,SAAS,GACb,OAAO,CAAC,SAAS;QACjB,kCAAkC,CAAC,MAAM,EAAE;YACzC,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;SAC3C,CAAC,CAAC;IAEL,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAE7D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AACpC,CAAC"}
@@ -7,4 +7,5 @@ export { NotificationFeed, type NotificationFeedProps } from './NotificationFeed
7
7
  export { NotificationInbox, type NotificationInboxProps } from './NotificationInbox.js';
8
8
  export { createNotificationsPollTransport, NOTIFICATIONS_CHANGED_EVENT, type NotificationsListClient, type NotificationsPollTransportOptions, notifyNotificationsChanged, } from './pollTransport.js';
9
9
  export { createNotificationStore, type NotificationAction, type NotificationItem, type NotificationPushInput, type NotificationState, type NotificationStore, type NotificationStoreConfig, type NotificationTone, type NotificationTransport, type NotificationTransportSync, useNotificationStore, } from './notificationStore.js';
10
+ export { useNotificationsBinding } from './useNotificationsBinding.js';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACL,0BAA0B,EAC1B,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,2BAA2B,GACjC,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,6BAA6B,EAC7B,sBAAsB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,GAClC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kCAAkC,EAClC,iBAAiB,EACjB,KAAK,mCAAmC,GACzC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACxF,OAAO,EACL,gCAAgC,EAChC,2BAA2B,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,iCAAiC,EACtC,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,EAC9B,oBAAoB,GACrB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACL,0BAA0B,EAC1B,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,2BAA2B,GACjC,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,6BAA6B,EAC7B,sBAAsB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,GAClC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kCAAkC,EAClC,iBAAiB,EACjB,KAAK,mCAAmC,GACzC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACxF,OAAO,EACL,gCAAgC,EAChC,2BAA2B,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,iCAAiC,EACtC,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,EAC9B,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC"}
@@ -7,4 +7,5 @@ export { NotificationFeed } from './NotificationFeed.js';
7
7
  export { NotificationInbox } from './NotificationInbox.js';
8
8
  export { createNotificationsPollTransport, NOTIFICATIONS_CHANGED_EVENT, notifyNotificationsChanged, } from './pollTransport.js';
9
9
  export { createNotificationStore, useNotificationStore, } from './notificationStore.js';
10
+ export { useNotificationsBinding } from './useNotificationsBinding.js';
10
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACL,0BAA0B,GAI3B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,6BAA6B,EAC7B,sBAAsB,GAIvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kCAAkC,EAClC,iBAAiB,GAElB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAA8B,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAA8B,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAA+B,MAAM,wBAAwB,CAAC;AACxF,OAAO,EACL,gCAAgC,EAChC,2BAA2B,EAG3B,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,EAUvB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACL,0BAA0B,GAI3B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,6BAA6B,EAC7B,sBAAsB,GAIvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kCAAkC,EAClC,iBAAiB,GAElB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAA8B,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAA8B,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAA+B,MAAM,wBAAwB,CAAC;AACxF,OAAO,EACL,gCAAgC,EAChC,2BAA2B,EAG3B,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,EAUvB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { type NotificationsBinding, type NotificationsBindingOptions } from './binding.js';
2
+ /**
3
+ * The React way to hold a {@link createNotificationsBinding}: built when the
4
+ * component COMMITS, disposed when it unmounts, `null` until then.
5
+ *
6
+ * const binding = useNotificationsBinding();
7
+ * if (!binding) return <span aria-hidden className={BELL_BOX} />;
8
+ * return <NotificationBell store={binding.store} />;
9
+ *
10
+ * 🔴 **Why this is a hook and not a `useMemo`.** `createNotificationsBinding`
11
+ * opens its stream (an EventSource plus a poll loop) the moment it is called,
12
+ * and React renders components it then throws away — a sibling suspending, an
13
+ * abandoned concurrent render, StrictMode's double render. Built during render,
14
+ * every discarded render left a stream open with nobody holding its `dispose`:
15
+ * panning family's tree leaked eight, six filled Chromium's per-host pool, and
16
+ * every later request on the page hung (forge task 2945). An effect only runs
17
+ * for a render React kept, and its cleanup is the dispose.
18
+ *
19
+ * `options` are read ONCE, when the binding is built — like `useState`'s
20
+ * initial value. A new object every render does not rebuild it; a callback in
21
+ * them should read changing state through a ref.
22
+ */
23
+ export declare function useNotificationsBinding(options?: NotificationsBindingOptions): NotificationsBinding | null;
24
+ //# sourceMappingURL=useNotificationsBinding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useNotificationsBinding.d.ts","sourceRoot":"","sources":["../../../src/react/notifications/useNotificationsBinding.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAEvH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,2BAA2B,GAAG,oBAAoB,GAAG,IAAI,CAU1G"}
@@ -0,0 +1,35 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { createNotificationsBinding } from './binding.js';
3
+ /**
4
+ * The React way to hold a {@link createNotificationsBinding}: built when the
5
+ * component COMMITS, disposed when it unmounts, `null` until then.
6
+ *
7
+ * const binding = useNotificationsBinding();
8
+ * if (!binding) return <span aria-hidden className={BELL_BOX} />;
9
+ * return <NotificationBell store={binding.store} />;
10
+ *
11
+ * 🔴 **Why this is a hook and not a `useMemo`.** `createNotificationsBinding`
12
+ * opens its stream (an EventSource plus a poll loop) the moment it is called,
13
+ * and React renders components it then throws away — a sibling suspending, an
14
+ * abandoned concurrent render, StrictMode's double render. Built during render,
15
+ * every discarded render left a stream open with nobody holding its `dispose`:
16
+ * panning family's tree leaked eight, six filled Chromium's per-host pool, and
17
+ * every later request on the page hung (forge task 2945). An effect only runs
18
+ * for a render React kept, and its cleanup is the dispose.
19
+ *
20
+ * `options` are read ONCE, when the binding is built — like `useState`'s
21
+ * initial value. A new object every render does not rebuild it; a callback in
22
+ * them should read changing state through a ref.
23
+ */
24
+ export function useNotificationsBinding(options) {
25
+ const optionsRef = useRef(options);
26
+ optionsRef.current = options;
27
+ const [binding, setBinding] = useState(null);
28
+ useEffect(() => {
29
+ const next = createNotificationsBinding(optionsRef.current);
30
+ setBinding(next);
31
+ return next.dispose;
32
+ }, []);
33
+ return binding;
34
+ }
35
+ //# sourceMappingURL=useNotificationsBinding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useNotificationsBinding.js","sourceRoot":"","sources":["../../../src/react/notifications/useNotificationsBinding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAA+D,MAAM,cAAc,CAAC;AAEvH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAqC;IAC3E,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC;IAC7B,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAA8B,IAAI,CAAC,CAAC;IAC1E,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,IAAI,GAAG,0BAA0B,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAC5D,UAAU,CAAC,IAAI,CAAC,CAAC;QACjB,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC,EAAE,EAAE,CAAC,CAAC;IACP,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt",
3
- "version": "5.70.0",
3
+ "version": "5.71.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
@@ -49,8 +49,13 @@ export interface NotificationsBinding {
49
49
  * transport feeding rows in, and the store's config hooks pushing
50
50
  * read/dismiss back out.
51
51
  *
52
- * const { store } = createNotificationsBinding();
53
- * <NotificationBell store={store} />
52
+ * const binding = useNotificationsBinding(); // in a component — never during render
53
+ * if (binding) return <NotificationBell store={binding.store} />;
54
+ *
55
+ * 🔴 This function CONNECTS when called, so a component must not call it while
56
+ * rendering: React discards renders, and each discarded one would leak an open
57
+ * stream. {@link useNotificationsBinding} builds it in an effect and disposes it
58
+ * on unmount; call this directly only outside React, or as a page-long singleton.
54
59
  *
55
60
  * ── The one asymmetry worth knowing ─────────────────────────────────────────
56
61
  * Rows flow IN through the transport; acknowledgments flow OUT through these
@@ -40,3 +40,4 @@ export {
40
40
  type NotificationTransportSync,
41
41
  useNotificationStore,
42
42
  } from './notificationStore.js';
43
+ export { useNotificationsBinding } from './useNotificationsBinding.js';
@@ -0,0 +1,107 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { act, cleanup, render } from '@testing-library/react';
3
+ import { StrictMode, Suspense } from 'react';
4
+ import type { NotificationsBindingClient } from './binding.js';
5
+ import type { NotificationTransport } from './notificationStore.js';
6
+ import { useNotificationsBinding } from './useNotificationsBinding.js';
7
+
8
+ afterEach(cleanup);
9
+
10
+ const client = {
11
+ queries: { list: async () => ({ items: [], hasMore: false }) },
12
+ mutations: {
13
+ markRead: async () => ({ changed: true }),
14
+ markAllRead: async () => ({ changed: 1 }),
15
+ setDone: async () => ({ changed: true }),
16
+ doneAll: async () => ({ changed: 1 }),
17
+ remove: async () => ({ changed: true }),
18
+ },
19
+ } as unknown as NotificationsBindingClient;
20
+
21
+ /** An inert transport that counts what it opened and what was closed — the
22
+ * stand-in for the EventSource a leaked binding leaves holding a connection. */
23
+ function countingTransport() {
24
+ const counts = { connects: 0, disposes: 0 };
25
+ const transport: NotificationTransport = {
26
+ connect: () => {
27
+ counts.connects += 1;
28
+ return () => {
29
+ counts.disposes += 1;
30
+ };
31
+ },
32
+ };
33
+ return { counts, transport, open: () => counts.connects - counts.disposes };
34
+ }
35
+
36
+ function Bell({ transport }: { transport: NotificationTransport }) {
37
+ const binding = useNotificationsBinding({ client, transport });
38
+ return <output>{binding ? 'bound' : 'pending'}</output>;
39
+ }
40
+
41
+ /** Suspends its first `times` renders, then renders nothing — the lazy
42
+ * switcher next to family's bell that made React discard the bell's render. */
43
+ function suspender(times: number) {
44
+ let left = times;
45
+ let release: () => void = () => {};
46
+ function Sibling(): null {
47
+ if (left > 0) {
48
+ throw new Promise<void>((resolve) => {
49
+ release = () => {
50
+ left -= 1;
51
+ resolve();
52
+ };
53
+ });
54
+ }
55
+ return null;
56
+ }
57
+ return { Sibling, release: () => release() };
58
+ }
59
+
60
+ describe('useNotificationsBinding', () => {
61
+ test('🔴 a render React throws away opens no stream — one binding, and it closes on unmount', async () => {
62
+ const { counts, transport, open } = countingTransport();
63
+ const { Sibling, release } = suspender(3);
64
+ const view = render(
65
+ <Suspense fallback={<span>loading</span>}>
66
+ <Bell transport={transport} />
67
+ <Sibling />
68
+ </Suspense>,
69
+ );
70
+ for (let retry = 0; retry < 3; retry += 1) {
71
+ await act(async () => {
72
+ release();
73
+ await Promise.resolve();
74
+ });
75
+ }
76
+
77
+ expect(view.getByText('bound')).toBeTruthy();
78
+ expect(counts.connects).toBe(1);
79
+ expect(open()).toBe(1);
80
+
81
+ view.unmount();
82
+ expect(open()).toBe(0);
83
+ });
84
+
85
+ test('StrictMode mounts twice and still leaves exactly one stream open', async () => {
86
+ const { transport, open } = countingTransport();
87
+ const view = render(
88
+ <StrictMode>
89
+ <Bell transport={transport} />
90
+ </StrictMode>,
91
+ );
92
+ await act(async () => {});
93
+ expect(view.getByText('bound')).toBeTruthy();
94
+ expect(open()).toBe(1);
95
+ view.unmount();
96
+ expect(open()).toBe(0);
97
+ });
98
+
99
+ test('a re-render with new options does not rebuild the binding', async () => {
100
+ const { counts, transport } = countingTransport();
101
+ const view = render(<Bell transport={transport} />);
102
+ await act(async () => {});
103
+ view.rerender(<Bell transport={transport} />);
104
+ await act(async () => {});
105
+ expect(counts.connects).toBe(1);
106
+ });
107
+ });
@@ -0,0 +1,35 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { createNotificationsBinding, type NotificationsBinding, type NotificationsBindingOptions } from './binding.js';
3
+
4
+ /**
5
+ * The React way to hold a {@link createNotificationsBinding}: built when the
6
+ * component COMMITS, disposed when it unmounts, `null` until then.
7
+ *
8
+ * const binding = useNotificationsBinding();
9
+ * if (!binding) return <span aria-hidden className={BELL_BOX} />;
10
+ * return <NotificationBell store={binding.store} />;
11
+ *
12
+ * 🔴 **Why this is a hook and not a `useMemo`.** `createNotificationsBinding`
13
+ * opens its stream (an EventSource plus a poll loop) the moment it is called,
14
+ * and React renders components it then throws away — a sibling suspending, an
15
+ * abandoned concurrent render, StrictMode's double render. Built during render,
16
+ * every discarded render left a stream open with nobody holding its `dispose`:
17
+ * panning family's tree leaked eight, six filled Chromium's per-host pool, and
18
+ * every later request on the page hung (forge task 2945). An effect only runs
19
+ * for a render React kept, and its cleanup is the dispose.
20
+ *
21
+ * `options` are read ONCE, when the binding is built — like `useState`'s
22
+ * initial value. A new object every render does not rebuild it; a callback in
23
+ * them should read changing state through a ref.
24
+ */
25
+ export function useNotificationsBinding(options?: NotificationsBindingOptions): NotificationsBinding | null {
26
+ const optionsRef = useRef(options);
27
+ optionsRef.current = options;
28
+ const [binding, setBinding] = useState<NotificationsBinding | null>(null);
29
+ useEffect(() => {
30
+ const next = createNotificationsBinding(optionsRef.current);
31
+ setBinding(next);
32
+ return next.dispose;
33
+ }, []);
34
+ return binding;
35
+ }